Spring Boot converts environment variables to property names automatically: SPRING_DATASOURCE_URL becomes spring.datasource.url. Underscores become dots, case is normalised. That mapping — relaxed binding — is why you rarely need to reference variables explicitly, and why a value you set can silently do nothing when the name is slightly off.
The other half of the picture is precedence. Spring Boot loads configuration from a long, deliberately ordered list of sources, and later sources override earlier ones. When a setting is not taking effect, the answer is nearly always that something further down the list is winning — and knowing the order turns that from a mystery into a lookup.
Table of contents
- Relaxed binding, precisely
- The precedence order
- Referencing variables in properties files
- Reading values in code
- Debugging a value that is being ignored
- Common failures
- How this fits the rest of the stack
- FAQ
Relaxed binding, precisely
Environment variables are conventionally uppercase with underscores. Spring properties are lowercase with dots and hyphens. Relaxed binding bridges them mechanically.
SPRING_DATASOURCE_URL -> spring.datasource.url
SPRING_DATASOURCE_USERNAME -> spring.datasource.username
SERVER_PORT -> server.port
APP_FEATURE_FLAGS_ENABLED -> app.feature-flags.enabled
MY_SERVICE_API_KEY -> my.service.api-key
The rules: uppercase to lowercase, underscore to dot. Hyphenated property names match too, because binding is done on a normalised form where hyphens are removed for comparison. That is why APP_FEATURE_FLAGS_ENABLED reaches app.feature-flags.enabled even though the hyphen has no representation in the variable name.
This is also the source of the most common confusion. app.featureFlags.enabled and app.feature-flags.enabled are the same property under relaxed binding, but only the kebab-case form is recommended, because it is the one guaranteed to round-trip through environment variables unambiguously.
For list properties, use indices: MY_APP_SERVERS_0_HOST maps to my.app.servers[0].host. It works and it is verbose enough that a single variable containing structured content is often better.
The precedence order
Spring Boot documents a specific ordering, and later sources win. The parts that matter in practice, from lowest priority upward:
- Default properties set programmatically.
@PropertySourceannotations.application.propertiesandapplication.ymlpackaged inside the jar.- Profile-specific files inside the jar, such as
application-prod.yml. application.propertiesoutside the jar, in the working directory or aconfig/subdirectory.- Profile-specific files outside the jar.
- OS environment variables.
- Java system properties set with
-D. - Command line arguments such as
--server.port=9000.
Two consequences are worth internalising. Environment variables beat everything packaged in the jar, which is exactly what you want for deployment — you ship one artifact and configure it per environment without rebuilding. And command line arguments beat environment variables, which makes them useful for a one-off override during debugging.
There is also a RandomValuePropertySource providing random.* properties, and JNDI attributes from java:comp/env in a servlet container.
When a value is not what you expect, do not guess at the order — ask the running application, which is covered below.
Referencing variables in properties files
Relaxed binding handles the automatic case. Sometimes you want to reference a variable with a name that does not map to your property, and the placeholder syntax covers that.
# application.properties
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DB_USER}
# with a default when the variable is absent
server.port=${PORT:8080}
app.timeout-seconds=${TIMEOUT:30}
app.mode=${APP_MODE:standard}
The same in YAML:
spring:
datasource:
url: ${DATABASE_URL}
username: ${DB_USER}
server:
port: ${PORT:8080}
The :default suffix is the part worth using consistently. Without it, a missing variable leaves the literal placeholder text as the value, and the failure surfaces later as a connection error against a host literally named ${DATABASE_URL} — confusing out of proportion to the mistake.
The PORT example is worth calling out. Many platforms assign a port through a PORT environment variable, and server.port=${PORT:8080} is the standard way to honour it while keeping a sensible local default.
Reading values in code
Three approaches, in increasing order of how much you should prefer them.
// 1. @Value on a field -- fine for one or two values
@Value("${app.api-key}")
private String apiKey;
@Value("${app.timeout-seconds:30}")
private int timeoutSeconds;
// 2. Environment abstraction -- for dynamic lookups
@Autowired
private Environment env;
String key = env.getProperty("app.api-key");
int timeout = env.getProperty("app.timeout-seconds", Integer.class, 30);
// 3. @ConfigurationProperties -- the one to reach for
@ConfigurationProperties(prefix = "app")
@Validated
public record AppProperties(
@NotBlank String apiKey,
@Positive int timeoutSeconds,
String mode
) {}
@ConfigurationProperties is the right default for anything beyond a couple of values. It groups related settings into one type, gives you constructor binding and immutability, supports validation annotations, and produces IDE completion through the configuration metadata.
The validation part is the practical win. With @Validated, a missing or malformed value fails at startup with a clear message naming the property. @Value fails at injection time or, worse, binds a wrong-but-parseable value and fails much later during a request.
Fail fast on configuration. An application that refuses to start because a required setting is absent is far easier to operate than one that starts and misbehaves under specific conditions.
Debugging a value that is being ignored
Spring Boot Actuator answers this directly, which beats reasoning about precedence.
management.endpoints.web.exposure.include=env,configprops
management.endpoint.env.show-values=when-authorized
Then /actuator/env lists every property source in priority order with the values each contributes, and /actuator/env/{property} shows which source won for one specific property. /actuator/configprops shows the bound @ConfigurationProperties objects as the application actually sees them.
Secure these endpoints. They expose configuration, and while Spring Boot masks values matching common secret patterns by default, an exposed env endpoint is an information disclosure and should not be publicly reachable.
Without Actuator, log the resolved values at startup — being careful not to log secrets:
@EventListener(ApplicationReadyEvent.class)
public void logConfig() {
log.info("server.port={}", env.getProperty("server.port"));
log.info("datasource host configured={}",
env.getProperty("spring.datasource.url") != null);
}
Log whether a secret is present, never its value. A password in a log file is a password in every log aggregator, backup, and support ticket the log ever touches.
Common failures
- Variable set, value ignored. Almost always a name mismatch under relaxed binding, or something higher in the precedence list overriding it. Check
/actuator/env/{property}. - Placeholder appears literally as the value. The variable was not set and had no default. Add
:defaultor make it required and validated. - Works locally, fails in a container. The container did not receive the variable.
printenvinside it, and check whether the orchestrator or compose file passes it through. - Profile-specific file not loading.
SPRING_PROFILES_ACTIVEis not set, or is set to a value that does not match the filename suffix. - Value binds as the wrong type.
@Valuewith a String field receiving a numeric setting.@ConfigurationPropertieswith a typed record catches this at startup. - Multi-line or structured values. Awkward in environment variables. Recent Spring Boot supports importing whole configuration blocks from a single variable via
spring.config.import=env:MY_CONFIGURATION, which handles the case cleanly.
How this fits the rest of the stack
The precedence order exists so that one artifact can be configured per environment without rebuilding — and that only pays off if the platform running it actually supplies the variables. On RunxBuild, environment variables are set per service and injected at runtime, so the same jar runs in staging and production with different SPRING_DATASOURCE_URL values and no rebuild between them, and rotating a credential is a restart rather than a new image. Java applications deploy from a GitHub repository with build logs, a live route, metrics, and rollback — services on RunxBuild covers variables alongside deploys and logs. When you are sizing a Spring Boot service with a managed Postgres or MySQL beside it, the RunxBuild hosting calculator shows each as a separate figure.
Useful related references:
- Django Environment Variables: django-environ, os.environ, and 12-Factor
- Python Environment Variables: os.environ, .env Files, and pydantic
- Docker Compose Environment Variables: env_file, .env, and Substitution
- Services on RunxBuild
FAQ
How do I set environment variables in Spring Boot?
Set them in the OS or platform using uppercase names with underscores — SPRING_DATASOURCE_URL maps automatically to spring.datasource.url through relaxed binding. You can also reference them explicitly in application.properties with ${DATABASE_URL} syntax, with a default like ${PORT:8080}.
What is relaxed binding in Spring Boot?
The mechanism that maps environment variable names to property names: uppercase becomes lowercase, underscores become dots, and hyphens in property names are matched by normalising them away. So APP_FEATURE_FLAGS_ENABLED binds to app.feature-flags.enabled.
Which configuration source wins in Spring Boot?
Later sources override earlier ones. From lowest to highest: packaged application.properties, profile-specific packaged files, external files, OS environment variables, Java system properties, then command line arguments. So environment variables beat anything inside the jar, and command line arguments beat everything.
Why is my Spring Boot environment variable being ignored?
Usually a name mismatch under relaxed binding, or a higher-priority source overriding it. Expose the Actuator env endpoint and query /actuator/env/{property} to see exactly which source supplied the value that won. Secure that endpoint — it exposes configuration.
Should I use @Value or @ConfigurationProperties?
@ConfigurationProperties for anything beyond a couple of settings. It groups related properties into one type, supports constructor binding and immutability, and works with validation annotations so a missing or malformed value fails at startup with a clear message rather than during a request.