How to Add Custom Properties to a Spring Boot App During Initialization (Without Losing Your Mind)
Learn practical methods to add custom properties to a Spring Boot application during initialization without breaking configuration precedence. This in-depth guide explains how Spring Boot loads configuration, how EnvironmentPostProcessor works, a...

I am Tuanh.net. As of 2024, I have accumulated 8 years of experience in backend programming. I am delighted to connect and share my knowledge with everyone.
1. Why “during initialization” is the only time custom properties actually feel magical
app.foo=bar in application.yml. It becomes about when Spring Boot builds the Environment, which PropertySource wins, and how to avoid accidentally overriding production config with your clever code. Spring Boot is powerful here because it explicitly supports customizing the Environment before the ApplicationContext is refreshed, so your custom properties can participate in the same externalized configuration system as everything else—profiles, config data imports, command line args, environment variables, and so on. That means you can compute values early and still allow operators to override them later, instead of hardcoding and praying.
1.1 The key idea: you’re not “adding properties”, you’re adding a PropertySource with precedence
PropertySources, and resolution is basically “first match wins”. If you add your custom values at the wrong position, you’ll either wonder why your value never shows up, or you’ll accidentally override values you absolutely should not override (like spring.datasource.password, which is a fun way to meet your security team). Spring Boot documents that command line properties have very high precedence over file-based sources, which is a good mental model: Boot is designed so that “more explicit, more local, more urgent” sources win. (Home)
1.2 The two startup hooks you should actually care about
Environment is being prepared, or when the ApplicationContext is being initialized. If you want the values to be available for profile selection, config loading decisions, or early logging setup, you target the Environment preparation phase with an EnvironmentPostProcessor. Spring Boot explicitly supports this extension point and expects you to register it via META-INF/spring.factories. (Home)If you only need the values once the context exists (but still early), an ApplicationContextInitializer can work, but it’s slightly later and easier to misuse if you depend on values that are supposed to influence the config loading pipeline.
1.3 What “good” looks like: computed defaults, override-friendly behavior, and predictable ordering
spring.config.import and profile-specific files enter the chat. (Home)
2. The most reliable approach: EnvironmentPostProcessor that adds a computed PropertySource
EnvironmentPostProcessor, register it in META-INF/spring.factories, and optionally control order. (Home)
2.1 A real-world example: derive app.instanceId, app.publicBaseUrl, and safe “defaults that can be overridden”
app.instanceId that is stable per pod/container when possible, and you want an app.publicBaseUrl computed from other known inputs. You also want operators to be able to override both with environment variables or config files if needed. The trick is to add your computed values as a low precedence source (so everything else can override it), but still early enough that your beans can use it normally.
package com.example.bootprops;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class ComputedDefaultsEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
public static final String SOURCE_NAME = "computed-defaults";
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
Map<string, object=""> computed = new HashMap<>();
String instanceId = firstNonBlank(
environment.getProperty("HOSTNAME"),
environment.getProperty("COMPUTERNAME"),
safeHostName(),
"local-" + UUID.randomUUID()
);
computed.put("app.instanceId", instanceId);
String scheme = firstNonBlank(environment.getProperty("app.publicScheme"), "https");
String host = firstNonBlank(environment.getProperty("app.publicHost"), environment.getProperty("HOSTNAME"), "localhost");
String port = firstNonBlank(environment.getProperty("app.publicPort"), environment.getProperty("server.port"), "8080");
boolean isStandardPort =
("https".equalsIgnoreCase(scheme) && "443".equals(port)) ||
("http".equalsIgnoreCase(scheme) && "80".equals(port));
String baseUrl = isStandardPort
? String.format("%s://%s", scheme, host)
: String.format("%s://%s:%s", scheme, host, port);
computed.put("app.publicBaseUrl", baseUrl);
MapPropertySource propertySource = new MapPropertySource(SOURCE_NAME, computed);
// Add LAST = lowest precedence. This makes them behave like defaults.
if (environment.getPropertySources().contains(SOURCE_NAME)) {
environment.getPropertySources().replace(SOURCE_NAME, propertySource);
} else {
environment.getPropertySources().addLast(propertySource);
}
}
@Override
public int getOrder() {
// Run late among post-processors so Boot loads its usual sources first,
// then we add defaults that can be overridden by higher-precedence sources.
return Ordered.LOWEST_PRECEDENCE;
}
private static String firstNonBlank(String... values) {
for (String v : values) {
if (v != null && !v.trim().isEmpty()) return v.trim();
}
return null;
}
private static String safeHostName() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (Exception ignored) {
return null;
}
}
}
MapPropertySource is the container for your properties, and the most important line is addLast. That single choice defines your precedence strategy: by putting the computed values at the end, you ensure anything loaded earlier with higher precedence can override them, which aligns with how Boot expects configuration to work across sources. (Home)
2.2 Registering it correctly so Spring Boot actually runs it
EnvironmentPostProcessor implementations to be registered in META-INF/spring.factories using the fully-qualified interface name as the key. (Home)
# file: src/main/resources/META-INF/spring.factories
org.springframework.boot.env.EnvironmentPostProcessor=
com.example.bootprops.ComputedDefaultsEnvironmentPostProcessor
spring.factories file end up in the final jar in the right place. The third suspect is ordering: if another post-processor adds a property source with higher precedence later, your defaults may be present but never visible.
2.3 Using the properties like normal in the rest of the application
@ConfigurationProperties, @Value, or read them from Environment. The whole point of doing this early is that you don’t need custom wiring everywhere else.
package com.example.bootprops;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "app")
public class AppRuntimeProperties {
private String instanceId;
private String publicBaseUrl;
public String getInstanceId() { return instanceId; }
public void setInstanceId(String instanceId) { this.instanceId = instanceId; }
public String getPublicBaseUrl() { return publicBaseUrl; }
public void setPublicBaseUrl(String publicBaseUrl) { this.publicBaseUrl = publicBaseUrl; }
}
APP_INSTANCEID or app.instanceId in a config file, that should win. Your post-processor becomes the “default value generator”, not the “configuration dictator”. That’s the difference between being helpful and being the reason someone rage-quits their deployment pipeline.
3. Precedence strategy: the part everyone skips until production breaks
application.yml, environment variables, or command line args depending on how and when Boot loads them. Spring Boot’s externalized configuration model is designed so that certain sources—like command line options—override others. (Home)So a good default rule is: if your properties are computed defaults, place them low. If your properties must override file config (rare, but sometimes used for forced-safe settings), place them high and document it loudly, because future you will forget and present you will pay.
3.1 When you actually do want high precedence (and how to not be reckless about it)
3.2 How profiles and Config Data loading change the game
4. Debugging and observability: proving your properties exist (without spamming logs)
4.1 A quick technique to validate values early
@Validated on configuration properties classes shines, because it turns “mysterious misbehavior” into “clear startup failure”.
5. Security and operational reality: don’t turn your “computed property” into a secret manager
6. Image links to support the explanation
https://dev.to/mohamed_el_laithy/7-spring-boot-lifecycle-stages-34do
https://sookocheff.com/post/java/understanding-springs-environment-abstraction/
https://docs.spring.io/spring-boot/reference/features/external-config.html
7. Closing thought: treat startup properties like infrastructure, not like a shortcut
Read more at : How to Add Custom Properties to a Spring Boot App During Initialization (Without Losing Your Mind)





