Configuration & Profiles Mastery
Type-safe configuration with @ConfigurationProperties, the full property precedence order, relaxed binding rules, profile groups, and keeping secrets out of your YAML.
On this page
Configuration is where a well-written application quietly becomes unmaintainable. Values drift into code, secrets end up in Git, and nobody can say which of four files supplied the value that is actually in effect. Spring Boot has a precise answer for all three problems.
Key Takeaways
- Property sources are consulted in a defined precedence order — command line beats environment beats packaged file, always.
- Relaxed binding means
app.rate-limit.max-requestsandAPP_RATELIMIT_MAXREQUESTSare the same key, which is what makes container deployment work. @ConfigurationProperties+@Validatedturns a bad config value into a startup failure instead of a 3am NullPointerException.- Profile-specific files merge onto the base file rather than replacing it.
- Secrets belong in the environment or a secret manager, never in a committed YAML file.
The precedence order
Spring Boot builds an Environment from an ordered list of property sources. Earlier entries win.
Trimmed to the ones that matter in practice:
The practical reading of this diagram: ship sane defaults inside the jar, override with environment variables per deployment. That combination requires no rebuild to change a value and no secret in the repository.
When you cannot tell where a value came from, ask the running application. /actuator/env lists
every property source in order and shows which one supplied the effective value, with sensitive keys
masked.
Relaxed binding
Spring Boot canonicalises property names before matching, so a single Java field accepts several spellings:
| Source | Form |
|---|---|
| YAML / properties | app.rate-limit.requests-per-minute |
| Camel case | app.rateLimit.requestsPerMinute |
| Environment variable | APP_RATELIMIT_REQUESTSPERMINUTE |
| Environment variable | APP_RATE_LIMIT_REQUESTS_PER_MINUTE |
Use kebab-case in YAML — it is the documented canonical form and the one the metadata processor generates. Environment variables only support uppercase and underscores, which is exactly why the relaxed rules exist.
Type-safe configuration
@ConfigurationProperties(prefix = "app.rate-limit")
@Validated
public record RateLimitProperties(
@DefaultValue("true")
boolean enabled,
@Min(1) @Max(100_000)
int requestsPerMinute,
@NotNull
Duration window,
@Valid
Redis redis
) {
public record Redis(@NotBlank String keyPrefix, @DefaultValue("3") int maxRetries) { }
}app:
rate-limit:
enabled: true
requests-per-minute: 600
window: 1m # bound to Duration; also accepts 60s, PT1M
redis:
key-prefix: 'rl:'
max-retries: 3Register it with @EnableConfigurationProperties(RateLimitProperties.class) on a configuration
class, or @ConfigurationPropertiesScan on the application class. Because the record is
@Validated, a requests-per-minute of zero fails the context startup with a message naming the
field — not a division-by-zero four hours later.
Add the metadata processor and the keys appear in IDE autocompletion:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>Spring Boot converts strings to rich types automatically: Duration (30s, 5m, PT1H),
DataSize (10MB), Period, enums, List, Map, and any type with a static valueOf or a
registered Converter.
Profiles
Activate with spring.profiles.active, which is normally supplied by the environment rather than
committed:
SPRING_PROFILES_ACTIVE=prod,eu-west java -jar app.jarProfile-specific files layer on top of the base file:
spring:
datasource:
hikari:
maximum-pool-size: 10
logging:
level:
com.acme: INFOspring:
datasource:
hikari:
maximum-pool-size: 50
# logging level is inherited from application.ymlTwo features are worth knowing beyond the basics.
Profile groups let one name activate several:
spring:
profiles:
group:
production: 'prod,metrics,cloud-config'Activating production now activates all four. This beats asking operators to remember a list.
Multi-document files keep small variations together using --- separators and an activation
condition:
server:
port: 8080
---
spring:
config:
activate:
on-profile: dev
server:
port: 8081Note spring.config.activate.on-profile — the older spring.profiles key is removed in Boot 3.
Conditional beans by profile
@Configuration
public class StorageConfig {
@Bean
@Profile("!prod")
public StorageClient localStorage(@Value("${app.storage.path}") Path path) {
return new FilesystemStorageClient(path);
}
@Bean
@Profile("prod")
public StorageClient s3Storage(S3Properties props) {
return new S3StorageClient(props.bucket(), props.region());
}
}@Profile accepts expressions: !prod, prod & eu, dev | test. Keep them simple — a bean that
needs a three-clause profile expression usually wants a @ConditionalOnProperty instead, which
states the actual condition rather than a proxy for it.
Keeping secrets out of the repository
Three approaches, in increasing order of rigour:
- Environment variables. Nothing in Git; the platform injects values. Adequate for most teams.
- Jasypt. Encrypted values sit in YAML as
ENC(...)and are decrypted at startup using a master password supplied by the environment. Useful when config must be versioned. - A secret manager. HashiCorp Vault, AWS Secrets Manager or Kubernetes External Secrets, wired through Spring Cloud Config or the Vault starter. Gives rotation, audit and short-lived dynamic credentials.
Whichever you choose, add a startup assertion that fails loudly when a required secret is absent
rather than defaulting to something harmless-looking. A @NotBlank on the properties record does
this for free.
What to take away
Put defaults in the jar, environment-specific values in the environment, and secrets in a secret
store. Bind everything through validated @ConfigurationProperties so a mistake stops the
deployment rather than reaching production. When something is unclear, /actuator/env tells you
exactly which source won.
Frequently Asked Questions
Should I use @Value or @ConfigurationProperties?
How do I override one property in a deployed container?
Do profile-specific files replace or merge with the base file?
Related tutorials
- Spring IoC Container InternalsThe Spring container from the inside: the full bean lifecycle in order, what BeanPostProcessor actually intercepts, every bean scope, and how circular dependencies are resolved.
- Spring AOP & Aspect-Oriented ProgrammingSpring AOP from pointcut syntax to proxy mechanics: the five advice types, writing annotation-driven aspects, aspect ordering, and why self-invocation silently does nothing.
- Spring Boot Auto-Configuration Deep DiveHow Spring Boot auto-configuration actually works: the import selector, the @Conditional family, ordering rules, the --debug report, and how to write your own starter.
- Actuator & Observability EndpointsEvery Actuator endpoint worth exposing, writing custom health indicators for Kubernetes probes, adding Micrometer metrics that answer real questions, and securing it all.