Skip to content

Spring Boot · Core

Spring Boot Configuration: Properties, Profiles, and Precedence

5 min read Updated 2026-06-26 Share:

Practice Properties & Profiles interview questions

One artifact, every environment

The promise of Spring Boot configuration is simple: build the jar once, then run that exact artifact in dev, staging and production by feeding it different external values. Interviewers probe this because getting it wrong leaks secrets into version control, or produces the dreaded "works on my machine but the property won't change in prod." This article covers the model end to end.

The Environment is a stack of property sources

Spring Boot merges many sources — command-line args, environment variables, system properties, profile files, the base application.properties — into a single Environment. The crucial rule is precedence: later (higher) sources override earlier ones.

Highest precedence (wins) ↓
1. Command-line args            --server.port=9000
2. SPRING_APPLICATION_JSON      inline JSON
3. OS environment variables     SERVER_PORT=9000
4. Java system properties       -Dserver.port=9000
5. Profile-specific files       application-prod.properties
6. application.properties/.yml  the base file
7. @PropertySource
8. Default properties
Lowest precedence ↑
# A command-line arg beats every file:
java -jar app.jar --server.port=9000

When a value "won't change," this list is the first thing to check — an env var or CLI arg above your file is almost always overriding it. That same rule is a feature: containers set environment variables and they cleanly win over the baked-in defaults.

@ConfigurationProperties vs @Value

Both read external values. @Value injects a single property; @ConfigurationProperties binds a whole group to a typed object.

// @Value — one or two values, SpEL placeholder with an optional default:
@Value("${app.mail.from}") String from;
@Value("${app.mail.retries:3}") int retries;

// @ConfigurationProperties — a cohesive block, type-safe and structured:
@ConfigurationProperties(prefix = "app.mail")
public class MailProperties {
    private String from;
    private int retries = 3;
    private List<String> bccList;  // structured types bind directly
    // getters/setters
}

@ConfigurationProperties wins for anything non-trivial: it gives type safety, relaxed binding, nested objects and lists, JSR-380 validation, and IDE metadata. Keep @Value for genuine one-offs.

Make it immutable with constructor binding

@ConfigurationProperties(prefix = "app.mail")
public record MailProperties(
    String from,
    @DefaultValue("3") int retries,
    List<String> bccList) { }

@Configuration
@EnableConfigurationProperties(MailProperties.class)
class ConfigPropsConfig { }

Since Spring Boot 3, a single non-default constructor triggers constructor binding automatically — no @ConstructorBinding needed. Records give you thread-safe, final configuration.

Validate at startup

@ConfigurationProperties(prefix = "app.mail")
@Validated
public class MailProperties {
    @NotBlank private String from;
    @Min(1) @Max(10) private int retries = 3;
    @Email private String replyTo;
}

A missing or invalid value now crashes the context at startup with a precise message — far better than a NullPointerException when the first email goes out.

Relaxed binding ties it all together

A single canonical property can be set by several naming conventions. This is what lets an environment variable configure a kebab-case property:

All of these bind to a field named fromAddress (prefix app.mail):
  app.mail.from-address   (kebab — canonical, use in files)
  app.mail.fromAddress    (camelCase)
  app.mail.from_address   (underscore)
  APP_MAIL_FROMADDRESS    (upper snake — how env vars must be written)

Write kebab-case in your files and let relaxed binding map SPRING_DATASOURCE_URL onto spring.datasource.url. Note relaxed binding applies to @ConfigurationProperties, not@Value — one more reason to prefer it.

Profiles: same code, different behavior

A profile is a named group of beans and properties active only in certain environments.

@Service @Profile("dev")  class InMemoryEmailService implements EmailService { }
@Service @Profile("prod") class SmtpEmailService     implements EmailService { }

Profiles drive two things: which beans get created (@Profile), and which property files load (application-{profile}.properties).

# application.properties (always loaded)
app.cache.ttl=60
spring.datasource.url=jdbc:h2:mem:devdb

# application-prod.properties (loaded only when prod is active; overrides the base)
app.cache.ttl=600
spring.datasource.url=jdbc:postgresql://db:5432/app

Activating profiles — from outside the jar

java -jar app.jar --spring.profiles.active=prod   # CLI
export SPRING_PROFILES_ACTIVE=prod                # env var (Docker/K8s)
java -Dspring.profiles.active=prod -jar app.jar   # system property

Set the active profile externally so the artifact stays environment-agnostic. In tests, use @ActiveProfiles("test").

Expressions and groups

@Bean @Profile("prod & !legacy") DataSource pooled() { /* ... */ }
@Bean @Profile("dev | test")     DataSource h2()     { /* ... */ }
# One name activates several profiles:
spring.profiles.group.production=prod,metrics,tracing
spring.profiles.active=production

If nothing is activated, the default profile runs — perfect for laptop-only convenience beans that should disappear the moment a real profile is chosen.

YAML or properties?

app:
  mail:
    from: [email protected]
    bcc-list:
      - [email protected]
      - [email protected]

YAML reads better for nested structures and lists; .properties is flatter and dodges YAML's indentation and type-coercion surprises (unquoted on/off become booleans). Pick one format per file — don't split the same profile across both.

Secrets never go in the repo

# Placeholders only — the real value is injected at runtime:
spring.datasource.password=${DB_PASSWORD}
app.api.key=${PAYMENT_API_KEY}
export DB_PASSWORD='s3cr3t'        # from a Docker/K8s secret or CI vault
export PAYMENT_API_KEY='pk_live_...'

For richer needs, reach for Spring Cloud Vault, AWS Secrets Manager, or Kubernetes Secrets. The committed application.properties should contain only placeholders and non-sensitive defaults.

Composing config with spring.config.import

spring.config.import=optional:file:./config/local.properties,\
                     optional:classpath:shared-defaults.yml
# Or external stores (with the right starter):
# spring.config.import=vault://secret/myapp

The Config Data API (2.4+) declaratively pulls in extra files and external systems, merged under the normal precedence rules. The optional: prefix avoids a startup failure when a source — like a developer-only override file — isn't present.

Recap

Externalized configuration lets one jar run everywhere by reading values from a precedence-ordered stack of property sources, where command-line args and environment variables override files. Use @ConfigurationProperties (ideally record-based and @Validated) for cohesive, type-safe config and reserve @Value for one-offs. Profiles select beans and property files per environment; activate them from outside the jar, compose them with groups and expressions, and lean on the default profile for local convenience. Keep secrets out of the repo with placeholders and a runtime secret source. Master precedence and relaxed binding and configuration stops being a guessing game.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel