Application Configuration: Properties, YAML, and Profiles
Every real application needs different configuration for different environments — a local database for dev, a connection pool for staging, a secret manager for prod. This article covers everything Spring Boot gives you to handle this cleanly.
application.properties vs application.yml
Spring Boot reads configuration from src/main/resources/application.properties (or .yml) by default. Both formats express the same thing:
application.properties:
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost:5432/orders
spring.datasource.username=app
spring.datasource.password=secret
spring.jpa.show-sql=false
application.yml:
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/orders
username: app
password: secret
jpa:
show-sql: false
YAML is generally preferred for nested properties — it’s less repetitive. Both are functionally equivalent. Pick one and be consistent.
The Property Hierarchy
Spring Boot resolves properties from many sources, in a specific order. Later sources override earlier ones. The most important levels are:
Priority (lowest → highest)
────────────────────────────────────
1. Default values in @ConfigurationProperties
2. application.properties / application.yml
3. Profile-specific: application-{profile}.properties
4. Environment variables
5. System properties (-Dproperty=value)
6. Command-line arguments (--property=value)
────────────────────────────────────
This means you can always override config from outside the JAR — critical for containers and CI/CD.
# Override at runtime without rebuilding
java -jar order-service.jar \
--spring.datasource.url=jdbc:postgresql://prod-db:5432/orders \
--server.port=9090
Profiles: Environment-Specific Configuration
Profiles let you have different configuration per environment without changing your code.
Creating Profile-Specific Files
Create files named application-{profile}.properties or application-{profile}.yml:
src/main/resources/
application.yml ← base config (all environments)
application-dev.yml ← dev overrides
application-staging.yml ← staging overrides
application-prod.yml ← prod overrides
application.yml (base):
server:
port: 8080
app:
order:
max-items: 50
currency: USD
spring:
jpa:
hibernate:
ddl-auto: validate
application-dev.yml:
spring:
datasource:
url: jdbc:h2:mem:orders
driver-class-name: org.h2.Driver
username: sa
password: ""
h2:
console:
enabled: true
jpa:
hibernate:
ddl-auto: create-drop # recreate schema on restart in dev
show-sql: true
logging:
level:
com.devopsmonk: DEBUG
org.springframework.web: DEBUG
application-prod.yml:
spring:
datasource:
url: ${DB_URL} # from environment variable
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
minimum-idle: 5
logging:
level:
root: WARN
com.devopsmonk: INFO
Activating a Profile
Via command-line (recommended for local dev):
java -jar order-service.jar --spring.profiles.active=dev
Via environment variable (recommended for containers):
export SPRING_PROFILES_ACTIVE=prod
java -jar order-service.jar
Via application.properties (for default local profile):
# Only set this in your local application.properties, never commit it
spring.profiles.active=dev
Via Maven at build time:
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
Profile Groups (Spring Boot 2.4+)
Group multiple profiles together:
spring:
profiles:
group:
prod: prod-db,prod-logging,prod-monitoring
staging: staging-db,staging-logging
Now --spring.profiles.active=prod activates all three prod profiles at once.
Environment Variable Binding
Spring Boot automatically maps environment variables to properties using relaxed binding:
| Environment Variable | Property |
|---|---|
SPRING_DATASOURCE_URL | spring.datasource.url |
SERVER_PORT | server.port |
APP_ORDER_MAX_ITEMS | app.order.max-items |
This is how you configure apps in Kubernetes without modifying the JAR:
# kubernetes deployment.yaml
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
- name: DB_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
@ConfigurationProperties — The Right Way to Read Config
Avoid injecting individual properties with @Value. Instead, bind a whole group of related properties to a typed class.
@ConfigurationProperties(prefix = "app.order")
@Validated
public class OrderProperties {
@NotNull
private String currency = "USD";
@Min(1)
@Max(1000)
private int maxItemsPerOrder = 50;
@DurationUnit(ChronoUnit.SECONDS)
private Duration processingTimeout = Duration.ofSeconds(30);
private RetryConfig retry = new RetryConfig();
// Nested config
public static class RetryConfig {
private int maxAttempts = 3;
private Duration backoff = Duration.ofMillis(500);
// getters and setters
}
// getters and setters
}
Register it:
@SpringBootApplication
@EnableConfigurationProperties(OrderProperties.class)
public class OrderServiceApplication { ... }
Or just add @Component to the properties class itself.
Configure it in YAML:
app:
order:
currency: EUR
max-items-per-order: 100
processing-timeout: 60s
retry:
max-attempts: 5
backoff: 1000ms
Inject it:
@Service
public class OrderService {
private final OrderProperties props;
public OrderService(OrderProperties props) {
this.props = props;
}
public Order createOrder(OrderRequest request) {
if (request.items().size() > props.getMaxItemsPerOrder()) {
throw new OrderTooLargeException(
"Max " + props.getMaxItemsPerOrder() + " items per order"
);
}
// ...
}
}
Why @ConfigurationProperties over @Value:
- Type-safe (Duration, List, Map — not just String)
- Validated at startup — fail fast on bad config
- IDE autocomplete (with
spring-boot-configuration-processor) - Testable (just instantiate and set values)
- Relaxed binding (camelCase, kebab-case, UPPER_SNAKE_CASE all work)
IDE Autocomplete for Custom Properties
Add the annotation processor:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
After rebuilding, your IDE shows autocomplete and documentation for app.order.* properties in application.yml.
Profile-Specific Beans
Sometimes you don’t just need different config — you need different implementations. Use @Profile:
// Used in dev — logs to console, never hits SES
@Component
@Profile("dev")
public class ConsoleEmailService implements EmailService {
public void send(Email email) {
log.info("DEV: Would send email to {} — subject: {}", email.to(), email.subject());
}
}
// Used in prod — real SES integration
@Component
@Profile("prod")
public class SesEmailService implements EmailService {
private final SesClient ses;
public void send(Email email) {
ses.sendEmail(/* ... */);
}
}
Only one is active at a time. The OrderService injects EmailService — it doesn’t care which one.
You can also use @Profile with logical expressions:
@Component
@Profile("!prod") // active in any profile that isn't prod
public class DevToolsConfig { ... }
@Component
@Profile("prod | staging") // active in prod OR staging
public class CloudMonitoring { ... }
Secrets: What Not to Put in Config Files
Never commit real secrets to application.properties or application.yml. These files go into git.
For local dev: use application-local.yml (add it to .gitignore):
# application-local.yml — NEVER commit this
spring:
datasource:
password: mysecretlocalpassword
For CI/CD and production: inject secrets as environment variables:
spring:
datasource:
password: ${DB_PASSWORD} # reads from env var
For production at scale: use a secrets manager:
- AWS Secrets Manager →
spring-cloud-aws - HashiCorp Vault →
spring-cloud-vault - Kubernetes Secrets → mounted as env vars or files
Practical: Configuring the order-service for Three Environments
Here’s what the complete config setup looks like for the sample order-service:
src/main/resources/
application.yml ← common defaults
application-dev.yml ← H2 in-memory, SQL logging, debug level
application-prod.yml ← PostgreSQL from env vars, connection pool tuned
application.yml:
server:
port: 8080
shutdown: graceful
spring:
application:
name: order-service
jpa:
open-in-view: false
hibernate:
ddl-auto: validate
app:
order:
currency: USD
max-items-per-order: 50
processing-timeout: 30s
application-dev.yml:
spring:
datasource:
url: jdbc:h2:mem:orderdb;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
username: sa
password:
h2.console.enabled: true
jpa:
show-sql: true
hibernate.ddl-auto: create-drop
logging:
level:
com.devopsmonk: DEBUG
application-prod.yml:
spring:
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
Run locally with dev profile:
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
Run in prod (Docker):
docker run \
-e SPRING_PROFILES_ACTIVE=prod \
-e DB_URL=jdbc:postgresql://db:5432/orders \
-e DB_USERNAME=app \
-e DB_PASSWORD=secret \
order-service:latest
Checking Active Configuration
During development, use the Actuator env endpoint to see resolved values:
management.endpoints.web.exposure.include=env,configprops
# See all resolved properties and their sources
curl http://localhost:8080/actuator/env | jq .
# See a specific property
curl http://localhost:8080/actuator/env/spring.datasource.url | jq .
The configprops endpoint shows all @ConfigurationProperties beans and their current values.
What You’ve Learned
application.ymlandapplication.propertiesare equivalent — YAML is preferred for nested config- Profile-specific files (
application-dev.yml,application-prod.yml) override base config per environment - Environment variables override file-based config — essential for containers
@ConfigurationPropertiesbinds groups of related properties to typed, validated classes@Profileswitches beans at runtime — use it for environment-specific implementations- Never commit secrets; inject them as environment variables or use a secrets manager
Next: Article 6 — Spring IoC and Dependency Injection — the container that makes all of this work.