Skip to main content

Command Palette

Search for a command to run...

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...

Published
10 min read
How to Add Custom Properties to a Spring Boot App During Initialization (Without Losing Your Mind)
T

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

Most Spring Boot configuration stories start the same way: you add a property, you inject it, you ship it… and then someone adds a “small” requirement like “this value must be calculated at startup”, “it depends on the active profile”, “it comes from a file mounted in Kubernetes”, or “we want to default it unless the user overrides it”. At that point, adding a property is no longer about writing 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

Spring Boot doesn’t treat properties as a single flat map. It treats them as a stack of 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

When people say “during initialization”, they often mix up multiple phases. In practice, you usually choose between adding your properties when the 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

A solid initialization-time property design usually has three traits. First, it computes a value that’s genuinely environment-specific, like a derived service URL, a build version, a region name, or a feature flag default. Second, it behaves like a default: if the operator sets the value via env var, config file, or command line, that should win. Third, it declares ordering so that it runs at a predictable time relative to Boot’s own config loading, which matters more than it sounds—especially when spring.config.import and profile-specific files enter the chat. (Home)

2. The most reliable approach: EnvironmentPostProcessor that adds a computed PropertySource

This approach is the closest thing Spring Boot has to “officially supported early customization”. You run before the context refresh, you can inspect existing properties, you can compute new ones, and you can insert them into the property source chain at exactly the precedence you want. Spring Boot’s API docs are blunt about it: implement 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”

Imagine a Spring Boot app deployed in multiple places: local dev, Docker, Kubernetes. You want an 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;
}
}
}

What this code is really doing is politely stepping into Spring Boot’s config party, bringing snacks, and sitting in the corner where it won’t block anyone’s view. The 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

Spring Boot expects 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

If your app starts and nothing happens, the first suspect is registration, not logic. The second suspect is packaging: make sure the class and the 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

Once your computed properties are in the environment, the rest is business as usual. You can inject them into @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; }
}

At runtime, if an operator sets 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

The most common failure mode is unintentionally overriding real config. If you add your property source at the beginning, you might override values from 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)

Sometimes you genuinely need to enforce something, like disabling an unsafe actuator endpoint in certain environments, or forcing a fallback if a required secret is missing. If you’re doing that, it’s better to fail fast with a clear error than silently override. A “silent override” is basically technical debt wearing an invisibility cloak.

3.2 How profiles and Config Data loading change the game

Since Spring Boot’s config data system can load profile-specific files and imports, your processor may run in a world where some sources exist and others are not fully resolved yet. That’s why Boot provides structured processing for config data and profile application, and why ordering matters when you depend on values that might come from config imports. (Home)In practice, this means your post-processor should either compute values from stable sources like environment variables and system properties, or it should treat other properties as optional hints and degrade gracefully.

4. Debugging and observability: proving your properties exist (without spamming logs)

When something goes wrong, you want answers to three questions: did my post-processor run, what values did it compute, and where did the final value come from. Spring Boot’s environment model makes “where did it come from” tricky unless you inspect the property sources. A practical trick is to log the property sources order at startup in a non-production profile, or expose a safe diagnostic endpoint that prints only non-sensitive keys. If you do it in production, do it with a denylist for secrets, because leaking config is an “achievement” nobody wants.

4.1 A quick technique to validate values early

If your property is required, validate it before the app begins accepting traffic. The best moment is still early in startup, so a bad configuration fails fast. This is also where @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

A common temptation is “I’ll just load secrets myself during startup and stuff them into the environment.” That can work technically, but it can also become a maintenance nightmare unless you treat it as a real integration with rotation, retries, timeouts, and secure logging. If you must inject secrets, be strict about masking logs and prefer platform mechanisms like Kubernetes Secrets, environment variables, or a dedicated config server. Your custom properties hook should feel like a small, well-tested utility, not a mini Vault clone that you’ll end up debugging at 3 a.m.

You asked for image links that make the concepts easier to visualize, so here are a few diagrams about lifecycle stages and environment/property sources. They’re useful for showing where “environment preparation” sits and why precedence matters.

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

Adding custom properties during initialization is one of those Spring Boot features that feels like a cheat code—until you realize the real power is not the code, it’s the discipline: precedence, override behavior, ordering, validation, and safe diagnostics. If you get those right, your app becomes easier to run in different environments, easier to debug, and less fragile when requirements evolve. If you get them wrong, you don’t just ship bugs—you ship confusion, which is the more expensive kind.

If you want to ask anything—like which precedence strategy fits your case, or how to compute properties from Kubernetes metadata or a CI build—comment below with your scenario and the properties you want to add.

Read more at : How to Add Custom Properties to a Spring Boot App During Initialization (Without Losing Your Mind)

More from this blog

T

tuanh.net

540 posts

Are you ready to elevate your Java, OOP, Spring, and DevOps skills? Look no further!