Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

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.

Beginner5 min readUpdated
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-requests and APP_RATELIMIT_MAXREQUESTS are the same key, which is what makes container deployment work.
  • @ConfigurationProperties + @Validated turns 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:

Property precedence: the first source that defines a key wins. Command line at the top, packaged defaults at the bottom.

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:

SourceForm
YAML / propertiesapp.rate-limit.requests-per-minute
Camel caseapp.rateLimit.requestsPerMinute
Environment variableAPP_RATELIMIT_REQUESTSPERMINUTE
Environment variableAPP_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

RateLimitProperties.java
@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) { }
}
application.yml
app:
  rate-limit:
    enabled: true
    requests-per-minute: 600
    window: 1m          # bound to Duration; also accepts 60s, PT1M
    redis:
      key-prefix: 'rl:'
      max-retries: 3

Register 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:

pom.xml
<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:

terminal
SPRING_PROFILES_ACTIVE=prod,eu-west java -jar app.jar

Profile-specific files layer on top of the base file:

application.yml
spring:
  datasource:
    hikari:
      maximum-pool-size: 10
logging:
  level:
    com.acme: INFO
application-prod.yml
spring:
  datasource:
    hikari:
      maximum-pool-size: 50
# logging level is inherited from application.yml

Two features are worth knowing beyond the basics.

Profile groups let one name activate several:

application.yml
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:

application.yml
server:
  port: 8080
---
spring:
  config:
    activate:
      on-profile: dev
server:
  port: 8081

Note spring.config.activate.on-profile — the older spring.profiles key is removed in Boot 3.

Conditional beans by profile

StorageConfig.java
@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:

  1. Environment variables. Nothing in Git; the platform injects values. Adequate for most teams.
  2. 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.
  3. 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?
Use @ConfigurationProperties for anything with more than one related key. It gives you a typed object, IDE autocompletion through the metadata processor, JSR-380 validation, and relaxed binding. @Value is fine for a single one-off value, but it fails at runtime rather than startup and offers no validation.
How do I override one property in a deployed container?
Set an environment variable. Relaxed binding maps APP_RATE_LIMIT_REQUESTS_PER_MINUTE onto app.rate-limit.requests-per-minute, and OS environment variables sit high in the precedence order, so they beat anything packaged in the jar without a rebuild.
Do profile-specific files replace or merge with the base file?
They merge. application.yml is always loaded, then application-prod.yml is layered on top, overriding only the keys it defines. Anything not mentioned in the profile file keeps its base value.

Related tutorials