Environment Preparation and Property Source Ordering
Environment Preparation and Property Source Ordering
The Annotation (or Abstraction)
Most engineers believe application.yml is where configuration lives. They know that application-dev.yml activates with --spring.profiles.active=dev, and that environment variables can override properties. The mental model is flat: a bag of key-value pairs with some override rules.
The actual model is a strictly ordered list of PropertySource objects, where the first match wins, and the ordering is neither alphabetical nor intuitive.
The Mechanism
Before the ApplicationContext exists, before any bean is defined, Spring Boot builds the Environment. The Environment holds an ordered list of PropertySource instances managed by MutablePropertySources. When you call environment.getProperty("server.port"), Spring iterates through every PropertySource in order and returns the first non-null value.
The default ordering (highest precedence first):
- Command-line arguments (
--server.port=9090) SPRING_APPLICATION_JSON(inline JSON from environment variable)- ServletConfig/ServletContext init parameters
- OS environment variables (
SERVER_PORT=9090) RandomValuePropertySource(only forrandom.*keys)- Profile-specific
application-{profile}.ymloutside the jar - Profile-specific
application-{profile}.ymlinside the jar application.ymloutside the jarapplication.ymlinside the jar@PropertySourceannotations on@Configurationclasses- Default properties (
SpringApplication.setDefaultProperties())
The class responsible for loading property files is org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor. It replaces the older ConfigFileApplicationListener and processes config files during the ApplicationEnvironmentPreparedEvent phase.
The Debuggable Demonstration
Print every PropertySource in the environment and its position in the precedence list:
// CORRECT: Print property source ordering at startup
@Component
public class PropertySourceInspector implements ApplicationRunner {
private final ConfigurableEnvironment environment;
public PropertySourceInspector(ConfigurableEnvironment environment) {
this.environment = environment;
}
@Override
public void run(ApplicationArguments args) {
System.out.println("=== Property Source Ordering (highest precedence first) ===");
int position = 0;
for (PropertySource<?> ps : environment.getPropertySources()) {
System.out.printf("[%2d] %s (%s)%n",
position++,
ps.getName(),
ps.getClass().getSimpleName());
}
}
}
Running the SaaS backend with --spring.profiles.active=prod --server.port=9090 produces:
=== Property Source Ordering (highest precedence first) ===
[ 0] commandLineArgs (SimpleCommandLinePropertySource)
[ 1] servletConfigInitParams (StubPropertySource)
[ 2] servletContextInitParams (StubPropertySource)
[ 3] systemProperties (PropertiesPropertySource)
[ 4] systemEnvironment (SystemEnvironmentPropertySource)
[ 5] random (RandomValuePropertySource)
[ 6] Config resource 'class path resource [application-prod.yml]' (OriginTrackedMapPropertySource)
[ 7] Config resource 'class path resource [application.yml]' (OriginTrackedMapPropertySource)
Position 0 wins. --server.port=9090 from the command line overrides server.port: 8080 in application.yml at position 7, which overrides server.port: 8000 in the code’s default properties if any existed at position 11.
The /actuator/env endpoint shows the same hierarchy in JSON form, with the resolved value and which source provided it.
The Failure Mode
The SaaS backend’s tenant isolation requires different database URLs per environment. The team sets SPRING_DATASOURCE_URL as an OS environment variable on the production server. They also have spring.datasource.url in application-prod.yml. The environment variable wins because OS environment variables (position 4) have higher precedence than profile-specific YAML files (position 6).
# application-prod.yml
spring:
datasource:
url: jdbc:postgresql://db-prod.internal:5432/saas_prod # This is ignored
# OS environment variable set by the deployment script
export SPRING_DATASOURCE_URL=jdbc:postgresql://db-staging.internal:5432/saas_staging
The application connects to the staging database in production. No error. No warning. The property resolution is working exactly as designed. The engineer who set the environment variable six months ago for a migration test forgot to remove it.
The Correct Pattern
// CORRECT: Use actuator/env to verify resolved property sources
// GET /actuator/env/spring.datasource.url
// Response:
// {
// "property": {
// "source": "systemEnvironment",
// "value": "jdbc:postgresql://db-staging.internal:5432/saas_staging"
// }
// }
The fix is operational: check /actuator/env after every deployment to verify that property values resolve from the expected source. Automate this in the deployment pipeline with a health check that validates critical properties against expected sources.
For the multi-tenant SaaS backend, encode the precedence rule in the team’s runbook: environment variables override everything except command-line arguments. If both are used for the same property, the behavior is deterministic but confusing. Pick one mechanism and enforce it.
Profile activation follows the same ordering rules. spring.profiles.active can be set as a command-line argument, an environment variable, or a YAML property. If set in multiple places, the highest-precedence source wins. Use spring.profiles.include to add profiles additively without worrying about override conflicts. The @Profile annotation on a @Configuration class only controls whether that class is processed. It does not affect property source ordering.