You own the config strategy for a service deployed to dev/staging/prod containers. How do you use Spring Boot's property sources and precedence to layer defaults, per-environment overrides, and secrets safely?
answer
- One immutable artifact, external overrides
- defaults yml -> profile files -> env vars -> secrets import
- env vars beat files (12-factor)
- spring.config.import=configtree/vault for secrets
- additional-location adds; tests pin via @TestPropertySource
basics
~20 sShip one immutable jar with safe defaults in application.yml, add profile-specific files for structural per-environment differences, and override the rest at runtime with environment variables (which outrank the files). Inject secrets from a secrets manager, never bake them into packaged files.
solid answer
~40 sPackage sensible, non-secret defaults in `application.yml` so the app runs anywhere. Capture structural per-environment differences in profile-specific files (`application-prod.yml`) activated with `spring.profiles.active`. For anything that varies per deployment — URLs, credentials, feature flags — rely on **environment variables**, because they cleanly outrank the packaged config data and are container-native (relaxed binding maps `SPRING_DATASOURCE_URL`). Keep secrets out of the image entirely: mount them from a secrets manager, e.g. via `spring.config.import=configtree:/run/secrets/` or Vault/cloud config, and never commit them. Use `spring.config.import` / `spring.config.additional-location` to bring in external config without discarding defaults. Reserve command-line args for one-off launches and `@TestPropertySource` to pin tests. The precedence order is the contract: one artifact, defaults at the bottom, environment overrides on top, secrets injected at runtime — nothing environment-specific baked into the build.
code
kotlin · 28 lines# application.yml (packaged: safe defaults + explicit imports)
spring:
application:
name: orders
datasource:
hikari:
maximum-pool-size: 10 # sane default, overridable per env
config:
# Pull secrets from a mounted directory of one-value files (K8s/Docker),
# optional so local dev without the mount still starts:
import: "optional:configtree:/run/secrets/"
---
# application-prod.yml (structural prod differences, NO secrets)
spring:
config:
activate:
on-profile: prod
jpa:
hibernate:
ddl-auto: validate
# Runtime (container env) supplies the per-deployment + secret values,
# which outrank the packaged files via precedence:
# SPRING_PROFILES_ACTIVE=prod
# SPRING_DATASOURCE_URL=jdbc:postgresql://prod-db:5432/orders
# /run/secrets/spring.datasource.password (file -> configtree)
#
# Nothing environment-specific or secret is baked into the image.go deeper
Knows to keep secrets out of the repo and that env vars override files.
Layers defaults + profile files + env vars and uses spring.config.import for extra config.
Designs the full layering, chooses additional-location vs location, and secures secret injection and Actuator /env.
Owns the contract end-to-end: one immutable artifact, precedence-driven overrides, secret governance, test determinism, and org-wide conventions to prevent override sprawl.
## Guiding principle: one immutable artifact, external overrides The entire property-source precedence design exists so you build **one** jar/image and reconfigure it per environment from the outside. Anything environment-specific baked into the build is an anti-pattern: it forces rebuilds, leaks staging/prod values into the artifact, and breaks reproducibility. ## Layering from bottom (defaults) to top (overrides) 1. **Packaged defaults — `application.yml` (classpath).** Non-secret, safe-to-run-anywhere values: pool sizes, timeouts, logging levels, feature-flag defaults. Lowest meaningful priority, so anything above can override. 2. **Profile-specific files — `application-{profile}.yml`.** Capture *structural* differences that are known at build time and don't leak secrets: which cache/queue implementation, which logging profile, DDL mode. Activate with `spring.profiles.active` (set via env var `SPRING_PROFILES_ACTIVE` in the container). Remember multiple active profiles apply last-wins. 3. **Environment variables — the primary override tier.** Per-deployment values: `SPRING_DATASOURCE_URL`, external service hosts, flag toggles. Env vars **outrank the config-data files**, are the 12-factor / container-native mechanism, and use relaxed binding. This is where dev vs staging vs prod actually diverge at runtime. 4. **Secrets — injected at runtime, never packaged.** Options: (a) `spring.config.import=configtree:/run/secrets/` to read a directory of files each holding one value (Docker/K8s secrets, mounted files); (b) Spring Cloud Vault / cloud config server via `spring.config.import=vault://...` or `configserver:`; (c) individual env vars fed from the platform's secret store. Never commit secrets to `application*.yml` in the repo or bake them into the image. 5. **Command-line args / SPRING_APPLICATION_JSON — situational top overrides.** CLI args for ad-hoc/manual launches and scripts; SPRING_APPLICATION_JSON when a platform only exposes one variable but you need a structured block. Both sit above env vars, so use sparingly and document them. 6. **Tests — `@TestPropertySource` / `@SpringBootTest(properties=...)`.** Highest at test time so tests are deterministic and independent of ambient env vars. ## Mechanisms that make this clean - **`spring.config.import`** — the modern, ordered way to pull in extra config (files, config trees, config servers, Vault). Importing is explicit and self-documenting inside `application.yml`. - **`spring.config.additional-location`** vs **`spring.config.location`** — prefer *additional* to add an external override folder without discarding the packaged defaults; `location` fully replaces the search list (use only when you truly want to disown defaults). - **`optional:` prefix** — mark not-always-present locations optional so missing files don't crash startup, while keeping mandatory ones strict. - **Relaxed binding + `@ConfigurationProperties`** — bind env vars cleanly into typed config beans; prefer this over scattered `@Value` for env-driven config. ## Governance and safety concerns (the principal lens) - **Precedence-aware review**: know that an ambient `SERVER_PORT` env var or a stray `-D` flag can silently override your file — audit the runtime environment, not just the repo. - **Determinism in tests/CI**: pin with `@TestPropertySource` so a developer's shell env can't change test behavior. - **Secret hygiene**: env vars appear in the process environment and process listings; treat them as sensitive, prefer mounted config trees / secret managers for high-value secrets, and rotate. - **Auditability**: expose the resolved environment carefully — the Actuator `/env` endpoint reveals property sources and their origins (great for debugging origin/precedence) but must be secured and value-sanitized in production. - **Avoid override sprawl**: too many layers (files + env + JSON + CLI) makes 'why is this value X?' hard. Keep a documented convention: defaults in yml, overrides via env, secrets via config import. - **Disabling sources**: `SpringApplication.setAddCommandLineProperties(false)` can lock out CLI overrides where they'd be a security/foot-gun risk. ## Anti-patterns to reject - Baking prod URLs/credentials into `application-prod.yml` committed to the repo. - Building a separate image per environment (defeats the whole model). - Relying on `@PropertySource` for environment overrides — it's low priority and added late, so it can't override the config-data files and can't set bootstrap keys. - Assuming SPRING_APPLICATION_JSON is low priority and using it for defaults (it's high priority). ## The one-sentence contract Defaults at the bottom (packaged yml), per-environment overrides via environment variables that outrank the files, secrets imported at runtime, tests pinned on top — one artifact, reconfigured from the outside, in the order Spring Boot documents.
- Why prefer environment variables over committing an application-prod.yml with real values?Env vars keep one immutable image reconfigurable per environment and keep secrets/prod URLs out of the repo and build artifact. They also outrank the packaged files by precedence, so overriding is clean and reproducible; a committed prod yml leaks values and forces per-environment builds.
- How would you inject database credentials without putting them in any yml or image layer?Mount them from a secrets manager or Docker/K8s secrets as files and load via spring.config.import=configtree:/run/secrets/, or use Spring Cloud Vault (spring.config.import=vault://...), or feed them as env vars sourced from the platform's secret store. All keep secrets out of the artifact and out of version control.
- A value is set in your committed application.yml but production shows a different value with no yml change deployed — how do you diagnose it?Something higher in precedence is overriding it: an env var (relaxed-bound), a -D system prop, SPRING_APPLICATION_JSON, or a CLI arg. Inspect the runtime environment and use the secured Actuator /env endpoint, which reports each property's active value and its originating source.
saying these in an interview costs you the question
- Committing real prod secrets/URLs into application-prod.yml
- Building a separate image per environment instead of overriding at runtime
- Using @PropertySource to override environment config (it can't beat config-data files)
- Leaving Actuator /env exposed and unsanitized in production
- Assuming committed files always win over ambient env vars