How Spring Boot's Configuration System Works
Spring Boot's configuration system is built on Spring's
Environment abstraction — a unified view over a prioritised
list of property sources. When you ask for
server.port, the Environment walks that list from
highest to lowest priority and returns the first value it finds. The
source that provides it (a .properties file, an
environment variable, a command-line argument) is transparent to the code
consuming it.
This is what makes twelve-factor-style configuration possible: the same application binary reads from a properties file in development, from environment variables in a container, and from a secrets manager in production — no code changes, only the active property sources differ.
Configuration File Formats
# application.properties — flat key/value
server.port=8080
server.servlet.context-path=/api
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=dbuser
# spring.datasource.password — see the secrets note below, NOT here in plain text
spring.jpa.hibernate.ddl-auto=validate # NEVER create/update in production
spring.jpa.open-in-view=false # recommended; see Spring Data JPA page
logging.level.root=INFO
logging.level.com.example=DEBUG
app.name=My Application
app.max-connections=100
# application.yml — YAML format, identical semantics, just hierarchical
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: dbuser
jpa:
hibernate:
ddl-auto: validate
open-in-view: false
app:
name: My Application
max-connections: 100
They are mechanically equivalent — Spring Boot loads both. YAML is more
readable for deep hierarchies and supports lists naturally, but a YAML
indentation error is a silent parse failure (wrong value, no exception).
Properties files are flat and verbose but harder to break accidentally.
Many teams pick YAML for readability and accept the indentation risk;
the only rule is not using both for the same profile in
the same project — when both exist, .properties wins.
Property Source Priority — What Overrides What
Spring Boot evaluates property sources in a fixed order. A source higher in this list (lower number = higher priority) shadows any value for the same key in a lower-priority source:
| Priority | Source | Typical use |
|---|---|---|
| 1 — highest | Command-line arguments (--key=value) | One-off overrides, debugging |
| 2 | SPRING_APPLICATION_JSON env var | JSON blob of overrides — common in some PaaS platforms |
| 3 | OS environment variables | Secrets and environment-specific values in containers |
| 4 | Profile-specific outside JAR (application-prod.properties beside the JAR) | Ops-managed config without rebuilding |
| 5 | Profile-specific inside JAR | Environment defaults bundled with the artifact |
| 6 | application.properties / .yml outside JAR | Ops override of bundled defaults |
| 7 — lowest useful | application.properties / .yml inside JAR | Bundled defaults and local dev values |
# Override via command-line — highest priority, wins over everything including env vars
java -jar app.jar --server.port=9090
# Override via environment variable — dots become underscores, uppercase
# spring.datasource.url → SPRING_DATASOURCE_URL
export SPRING_DATASOURCE_URL=jdbc:postgresql://prod-host:5432/proddb
java -jar app.jar
Spring's relaxed binding translates spring.datasource.url
to the environment variable SPRING_DATASOURCE_URL
automatically. Hyphens in property names (max-connections)
also become underscores: APP_MAX_CONNECTIONS. This is what
makes containerised deployment work cleanly — no code changes, just
environment variables injected by Kubernetes secrets or your CI/CD
pipeline.
Accessing Properties in Code
@Value — for a single property in any bean
@Component
public class MyService {
@Value("${app.name}")
private String appName;
@Value("${app.max-connections:50}") // :50 is the default if key is absent
private int maxConnections;
}
First, it cannot be used on static fields — the injection
happens at bean instantiation time, not at class-load time. Second, it
provides zero type safety and no IDE-assisted autocomplete — a typo in
the key string produces an IllegalArgumentException at
startup rather than a compile error. For anything beyond a single
one-off value, @ConfigurationProperties is the better
choice.
@ConfigurationProperties — type-safe binding for a group of related properties
// Modern idiom: a record with @ConfigurationProperties + @Validated
@ConfigurationProperties(prefix = "app")
@Validated // fails at startup with a clear message if any constraint is violated
public record AppProperties(
@NotBlank String name,
@Min(1) @Max(1000) int maxConnections,
@NotNull Security security
) {
public record Security(
@NotBlank String secretKey,
@Positive long tokenExpiry
) {}
}
# application.yml — maps directly to the record above
app:
name: My Application
max-connections: 100
security:
secret-key: "${APP_SECRET_KEY}" # resolved from env var — NOT hardcoded here
token-expiry: 3600000
// Enable scanning for @ConfigurationProperties records
@SpringBootApplication
@ConfigurationPropertiesScan
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}
// Inject as any other bean
@Service
public class TokenService {
private final AppProperties props;
public TokenService(AppProperties props) { this.props = props; }
public long tokenExpiry() { return props.security().tokenExpiry(); }
}
<!-- pom.xml — optional but highly recommended -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
At compile time this generates
META-INF/spring-configuration-metadata.json, which IntelliJ
and VS Code read to provide autocomplete, type hints, and documentation
pop-ups for your own app.* keys in
application.properties/.yml. Without it, only
Spring Boot's own properties get autocomplete — yours don't.
<optional>true</optional> ensures it's a
compile-only tool that doesn't end up in downstream dependencies or the
final JAR.
The classic approach — a @Component @ConfigurationProperties
mutable class with getters and setters — works, but gives you a mutable
configuration object that any bean can accidentally modify at runtime.
A record is immutable by construction: fields are set once
during binding and cannot change. Additionally,
@Validated on a @ConfigurationProperties bean
fails the application at startup with a readable
message if a required value is absent or out of range — rather than
failing at the first runtime call that hits the missing config,
potentially hours into a production deployment.
Secrets — What Never Goes in a Properties File
Database passwords, API keys, JWT signing keys, and any other credential
in a .properties or .yml file that reaches
version control are compromised — not "at risk of being compromised."
Git history is permanent and often broader in access than the running
application. This is one of the most frequent findings in security
audits of Java applications.
The correct pattern at each environment level:
| Environment | Correct approach |
|---|---|
| Local dev | .env file or IDE run configuration env vars — file added to .gitignore, never committed |
| CI/CD pipeline | CI platform secret variables (GitHub Actions secrets, GitLab CI variables) injected as env vars at runtime |
| Container / Kubernetes | Kubernetes Secret objects mounted as env vars or volume files — never baked into the image |
| Production | HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, or similar — Spring Cloud Vault integrates directly with Spring's Environment |
# What a properties file SHOULD contain for a secret:
spring.datasource.password=${DB_PASSWORD}
# The actual value lives in an environment variable, a Kubernetes Secret,
# or a secrets manager — never in this file.
Interview Questions
Q: What's the difference between @Value and @ConfigurationProperties?
@Value injects a single property by key string — convenient for
one-off values, no compile-time safety. @ConfigurationProperties
binds a whole group of related properties to a typed class or record, with
IDE autocomplete, refactoring support, and optional validation at startup.
Q: If the same property is set in application.properties and as an environment variable, which wins?
The environment variable — it sits higher in the property source priority
order than the bundled properties file. Command-line arguments
(--key=value) are higher still and override both.
Q: Why is @Validated on @ConfigurationProperties preferable to validating config values at first use?
Startup-time validation fails the application immediately, before any traffic
is served, with a clear message naming the missing or invalid property.
Validating at first use means a misconfigured production deployment can start
successfully, pass health checks, and then fail mid-flight on the first
request that hits the code path needing the bad value — potentially after
the deployment is considered "done" and the previous version has been
terminated.
Q: How does Spring Boot's relaxed binding work, and why does it matter for containerised deployments?
Spring accepts a property value under multiple naming conventions
simultaneously: app.maxConnections,
app.max-connections, and the environment variable
APP_MAX_CONNECTIONS all resolve to the same binding target.
This matters for containers because OS environment variables cannot contain
dots or hyphens — relaxed binding means the same property the application
reads as app.max-connections in a YAML file is trivially
injectable as APP_MAX_CONNECTIONS in a Kubernetes secret or
Docker run command, with no application code changes.