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.

Syntaxproperty 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:

  1. Defaults set in code with SpringApplication.setDefaultProperties.
  2. @PropertySource files on configuration classes.
  3. Config data files: application.yaml inside the JAR, then profile files inside the JAR, then the same two outside the JAR.
  4. Operating system environment variables.
  5. Java system properties (-Dserver.port=9000).
  6. SPRING_APPLICATION_JSON.
  7. 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

Example · bash
# 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.jar

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

Example · yaml
# 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.

Example · java
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.

Example · text
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

  • Be the first to comment on this lesson.