Configuration Precedence and Environment Variables
Know which property source wins in Spring Boot, how environment variable names map to property keys, and how to override safely in containers.
property key -> environment variable
spring.datasource.url -> SPRING_DATASOURCE_URL
app.orders.page-size -> APP_ORDERS_PAGESIZE
app.hosts[0] -> APP_HOSTS_0_The same key can be set in many places: the packaged file, a profile file, an environment variable, a command-line argument. Spring Boot merges them all into one Environment, and when two sources set the same key the higher-priority source wins. Knowing the order is how you answer "why is it still using the old value?"
The order that matters in practice
From lowest to highest priority, the sources you will actually meet are:
- Defaults set in code with
SpringApplication.setDefaultProperties. @PropertySourcefiles on configuration classes.- Config data files:
application.yamlinside the JAR, then profile files inside the JAR, then the same two outside the JAR. - Operating system environment variables.
- Java system properties (
-Dserver.port=9000). SPRING_APPLICATION_JSON.- Command-line arguments (
--server.port=9000).
In tests, @SpringBootTest(properties = ...), @DynamicPropertySource and @TestPropertySource sit above all of these.
Mapping environment variables
Environment variables cannot contain dots or dashes on most systems, so Boot maps them with three rules: replace dots with underscores, remove dashes, and use upper case. spring.datasource.url becomes SPRING_DATASOURCE_URL and app.orders.page-size becomes APP_ORDERS_PAGESIZE. A list index is surrounded by underscores: APP_CORS_ALLOWEDORIGINS_0_ sets app.cors.allowed-origins[0].
This mapping is why canonical kebab-case keys matter. @ConfigurationProperties fields and kebab-case @Value keys match the environment variable; a key written in camelCase inside @Value may not.
Production consequences
- Environment variables beat the packaged file, which is what you want in containers: the image is the same everywhere and only variables change.
- A forgotten variable in a deployment template silently overrides a newer value in the file. When a change "does nothing", look at the environment first.
- Map keys bound from environment variables are always lower case, and each underscore becomes a dot, so maps whose keys need upper case, dashes or underscores should be set from a file, not from variables.
The /actuator/env endpoint lists each property source in order and shows which one supplied a key. Values are masked by default; the key names and sources are what you need for debugging anyway.
Example
# application.properties inside the JAR says: server.port=8080, app.orders.page-size=50
# 1. An environment variable overrides the packaged file
export SERVER_PORT=8081
export APP_ORDERS_PAGESIZE=25
java -jar orders-api.jar # port 8081, page size 25
# 2. A system property overrides the environment variable
java -Dserver.port=8082 -jar orders-api.jar # port 8082
# 3. A command-line argument overrides both
java -Dserver.port=8082 -jar orders-api.jar --server.port=8083 # port 8083
# 4. SPRING_APPLICATION_JSON sets several (or nested) keys in one variable
SPRING_APPLICATION_JSON='{"app":{"orders":{"page-size":10}}}' java -jar orders-api.jarWhen to use it
- Running the same container image in staging and production with only SPRING_DATASOURCE_URL and SPRING_PROFILES_ACTIVE changing.
- Finding out why a log level change in application.yaml has no effect because a LOGGING_LEVEL_* variable in the deployment overrides it.
- Pinning test configuration with @SpringBootTest properties so a developer's shell variables cannot change test results.
More examples
Variables in a Compose file
The image is identical in every environment; only these variables change. `LOGGING_LEVEL_COM_EXAMPLE_ORDERS` maps to `logging.level.com.example.orders`, because each dot in the logger name becomes an underscore. The password comes from the shell that runs Compose, so it is not written into the file.
# compose.yaml
services:
api:
image: registry.example.com/orders-api:1.8.2
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/orders
SPRING_DATASOURCE_USERNAME: orders
SPRING_DATASOURCE_PASSWORD: ${ORDERS_DB_PASSWORD}
APP_ORDERS_PAGESIZE: "25"
LOGGING_LEVEL_COM_EXAMPLE_ORDERS: INFO
db:
image: postgres:17
environment:
POSTGRES_DB: orders
POSTGRES_USER: orders
POSTGRES_PASSWORD: ${ORDERS_DB_PASSWORD}Overriding properties in a test
Properties set by the test sit above files, environment variables and command-line arguments, so a developer's own `SPRING_PROFILES_ACTIVE` or `SERVER_PORT` cannot change the test result. Use `properties` for fixed values and `@DynamicPropertySource` for values known only at runtime, such as a Testcontainers port.
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
@SpringBootTest(properties = {"app.region=eu-west-2", "app.orders.page-size=5"})
class PropertyPrecedenceTest {
@SpringBootConfiguration
static class TestApp {
}
// Computed at runtime, for example from a container's mapped port.
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
int port = 54321;
registry.add("app.inventory.base-url", () -> "http://localhost:" + port);
}
@Autowired
Environment environment;
@Test
void testPropertiesWinOverFilesAndEnvironmentVariables() {
assertThat(environment.getProperty("app.region")).isEqualTo("eu-west-2");
assertThat(environment.getProperty("app.orders.page-size", Integer.class)).isEqualTo(5);
assertThat(environment.getProperty("app.inventory.base-url")).isEqualTo("http://localhost:54321");
}
}Name mapping at a glance
Dots become underscores, dashes disappear, everything is upper case. Check a variable against this table before blaming the code. A misspelt variable such as `SPRING_DATASOUCE_URL` is not an error; it is simply never read, and the packaged value stays in force.
Property key Environment variable
---------------------------------------- ---------------------------------------
server.port SERVER_PORT
spring.datasource.url SPRING_DATASOURCE_URL
spring.jpa.open-in-view SPRING_JPA_OPENINVIEW
spring.threads.virtual.enabled SPRING_THREADS_VIRTUAL_ENABLED
app.orders.page-size APP_ORDERS_PAGESIZE
app.cors.allowed-origins[0] APP_CORS_ALLOWEDORIGINS_0_
logging.level.org.hibernate.SQL LOGGING_LEVEL_ORG_HIBERNATE_SQL
Discussion