Application Properties

How Spring Boot resolves configuration — property sources, precedence, type-safe binding, and what never belongs in a properties file

← Back to Index

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
properties vs yml — which to choose

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:

PrioritySourceTypical use
1 — highestCommand-line arguments (--key=value)One-off overrides, debugging
2SPRING_APPLICATION_JSON env varJSON blob of overrides — common in some PaaS platforms
3OS environment variablesSecrets and environment-specific values in containers
4Profile-specific outside JAR (application-prod.properties beside the JAR)Ops-managed config without rebuilding
5Profile-specific inside JAREnvironment defaults bundled with the artifact
6application.properties / .yml outside JAROps override of bundled defaults
7 — lowest usefulapplication.properties / .yml inside JARBundled 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
Environment variable naming: dots → underscores, uppercase

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;
}
@Value has two important limitations

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(); }
}
Add the annotation processor for IDE autocomplete on your own properties
<!-- 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.

Why records beat the mutable class approach

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

A properties file committed to git is a public secret — even in a private repo

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:

EnvironmentCorrect approach
Local dev.env file or IDE run configuration env vars — file added to .gitignore, never committed
CI/CD pipelineCI platform secret variables (GitHub Actions secrets, GitLab CI variables) injected as env vars at runtime
Container / KubernetesKubernetes Secret objects mounted as env vars or volume files — never baked into the image
ProductionHashiCorp 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

🎓 Junior level

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.

🔥 Senior level

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.