Configuration & Profiles
Everything that legitimately differs between deployments of one build — and how Spring Boot gathers it. The Environment as a priority stack of property sources, application.yml and relaxed binding, @Value versus type-safe @ConfigurationProperties records with validation, profiles and groups, secrets for containers, and config-driven wiring — capped by a plain-Java mini Environment you can run.
Configuration Is What Varies Between Deploys
Every Spring Boot service runs in at least four places: a laptop, CI, a shared staging cluster and production. The code is identical in all four. The values are not: the database URL, the downstream payment host, the thread-pool size, the log level, the feature flags, and above all the credentials. Configuration is everything that legitimately differs between deployments of the same build. If you have to rebuild the jar to change it, it was code, not configuration.
Boot's answer is the Environment: an ordered stack of PropertySource objects, each a bag of name/value pairs (command line, OS environment variables, application.yml, ...). A lookup walks the stack from highest to lowest priority and returns the first hit. Everything on this page — profiles, @Value, @ConfigurationProperties, secrets, @ConditionalOnProperty — is a way of writing into or reading from that one stack.
apiKey).
The Environment Is a Priority Stack
PropertySource is a named collection of properties (commandLineArgs, systemEnvironment, Config resource 'class path resource [application.yml]', ...)getProperty(name) → value or null. It has no idea the others existEnvironment holds a MutablePropertySources list in priority orderenv.getProperty("server.port") iterates the list top to bottom and returns the first non-null value — first match wins, no mergingSpringApplication.run() builds the Environment before the ApplicationContext existsConfigDataEnvironmentPostProcessor, so properties can influence which beans, profiles and auto-configurations are even consideredSERVER_PORT, server.port and server-port are the same propertyConfigurationPropertySources wraps every source with an adapter that applies relaxed-binding rules at lookup time (Chapter 04)${other.key:default} placeholders inside values are resolved lazilyPropertySourcesPropertyResolver, and fails with IllegalArgumentException if no default is given and the key is missing@Value and the Binder (used by @ConfigurationProperties)server.port walks the stack top-down and stops at the first hitTwo properties of this design explain nearly every configuration surprise. First, lookup is per key, not per file: if application-prod.yml overrides only server.port, every other key still falls through to application.yml. Second, values are never merged across sources: a list or map defined in two places is replaced wholesale by the higher-priority source (the one exception being maps bound via Binder, which are merged key by key — lists are not).
Where Values Come From, and Who Wins
Boot documents a fixed order for about seventeen kinds of source. You rarely need all of them; the useful mental shortcut is the closer a source is to the running process, the higher its priority. Command-line flags beat environment variables, which beat files in the working directory, which beat files packaged inside the jar, which beat defaults.
| Priority | Source | Typical use | Notes |
|---|---|---|---|
| 1 (highest) | @TestPropertySource, @SpringBootTest(properties=...) | Tests | Only present when a test context is created |
| 2 | Command-line arguments --key=value | One-off override, java -jar | Disable with setAddCommandLineProperties(false) |
| 3 | SPRING_APPLICATION_JSON (env var or spring.application.json) | Inject a JSON blob from a platform that only offers one variable | Flattened into normal properties |
| 4 | JVM system properties -Dkey=value | JVM-level flags, legacy launchers | Visible to every library, not just Spring |
| 5 | OS environment variables | Containers, Kubernetes, CI | Name mapping: SERVER_PORT → server.port |
| 6 | ${random.*} source | Random port/secret in tests | ${random.int(1000,2000)}, ${random.uuid} |
| 7 | Profile-specific files outside the jar | Ops-managed overrides next to the jar | ./config/application-prod.yml |
| 8 | Profile-specific files inside the jar | Environment defaults shipped with the build | application-prod.yml on the classpath |
| 9 | Base application.yml/.properties outside the jar | Ops-managed base values | ./application.yml |
| 10 | Base application.yml/.properties inside the jar | Sane defaults | Lowest of the "real" config files |
| 11 | @PropertySource on @Configuration classes | Extra .properties files | No YAML support; loaded too late for logging.*, spring.main.* |
| 12 (lowest) | SpringApplication.setDefaultProperties(...) | Programmatic defaults before startup | Only way to default something before any file is read |
File locations. Boot searches these in order, and later locations override earlier ones: classpath:/, classpath:/config/, file:./, file:./config/, file:./config/*/. Within a single location, .properties beats .yml for the same key. Three knobs change the search:
| Property | Effect | Example |
|---|---|---|
spring.config.name | Base name instead of application | --spring.config.name=orders → orders.yml |
spring.config.location | Replaces the default locations | --spring.config.location=file:/etc/orders/ |
spring.config.additional-location | Adds locations above the defaults | --spring.config.additional-location=file:/etc/orders/ |
spring.config.import | Pull in more files, config trees, vault sources from inside a config file | spring.config.import=optional:file:/etc/orders/local.yml |
| Step | What Boot does | Class involved |
|---|---|---|
| 1 | Create the Environment with servlet/system/env sources | SpringApplication.prepareEnvironment |
| 2 | Read command-line args into commandLineArgs | DefaultApplicationArguments |
| 3 | Locate and load application.* (no profile) and resolve spring.config.import | ConfigDataEnvironmentPostProcessor |
| 4 | Read spring.profiles.active / groups → load application-{profile}.* files | ConfigDataEnvironment |
| 5 | Insert each file as a PropertySource at the right priority | MutablePropertySources |
| 6 | Only now create the ApplicationContext; beans can read the finished stack | SpringApplication.createApplicationContext |
- Setting
spring.profiles.activein a profile-specific file →InvalidConfigDataPropertyException: Property 'spring.profiles.active' imported from location 'class path resource [application-prod.yml]' is invalid in a profile specific resource. Profiles must be decided before profile files load; set them inapplication.yml, an env var or on the command line. - Expecting two files to merge a list.
payment.currencies: [USD, EUR]inapplication.ymland[GBP]inapplication-prod.ymlgives[GBP], not three entries. Symptom: a currency silently disappears in prod. - Using
@PropertySourcefor a.ymlfile — the YAML is parsed as a flat properties file and no key resolves. Usespring.config.importinstead. - Forgetting
optional:on an import that may legitimately be missing →ConfigDataLocationNotFoundException: Config data location 'file:./local-overrides.yml' cannot be foundand the app refuses to start. Equally, omitoptional:for a secrets import so a missing secret fails the deploy loudly. - Stale
application.ymlin the working directory overriding the jar's: a forgotten./config/application.ymlon one server explains "works on staging-1, not staging-2". Diagnose with/actuator/env/<key>, which lists every source that defines the key and which one wins.
application.yml and Relaxed Binding
YAML is a nicer way to write the same flat key/value pairs. Boot flattens nested maps into dotted keys and lists into indexed keys (payment.currencies[0]), then everything is a property like any other. That flattening is also the source of the classic YAML mistakes.
When the Binder maps properties to a Java field it compares names in a canonical form (lower case, dashes and underscores removed). So the same value can be supplied in whatever style suits the source. In .properties/.yml use kebab-case; in env vars use UPPER_SNAKE.
Documents are processed top to bottom, so a later document overrides an earlier one when its on-profile condition matches. The old spring.profiles: dev key (Boot ≤ 2.3) was replaced by spring.config.activate.on-profile; the old key now fails at startup in Boot 3.
- YAML type coercion.
enabled: yes/onare booleans in YAML 1.1;version: 1.10becomes the float1.1;zip: 01234loses the zero (octal-looking). Quote anything that must stay a string:zip: "01234". - A colon or
#inside an unquoted value.password: abc#123silently storesabc— the rest is a YAML comment. Quote it, and better, do not keep passwords in files. - Tabs or wrong indentation →
org.yaml.snakeyaml.scanner.ScannerExceptionwrapped inConfigDataResourceNotFoundException/Failed to load property source from location 'classpath:/application.yml'. Yaml needs spaces only. - Env var with a dash in the name (
PAYMENT_MAX-RETRIES) — most shells cannot even export it. UsePAYMENT_MAXRETRIES; Boot strips dashes from the property side when matching. - Case sensitivity in
@Valuewith camelCase keys.@Value("${payment.maxRetries}")works for amax-retriesproperty only because Boot's source adapters normalise; prefer the canonical kebab-case form in@Valueso env-var overrides keep matching.
@Value vs @ConfigurationProperties
There are two ways to get a property into a bean. @Value injects one value through the BeanPostProcessor AutowiredAnnotationBeanPostProcessor, resolving the placeholder and optionally a SpEL expression. @ConfigurationProperties binds a whole prefix to a typed object through the Boot Binder (ConfigurationPropertiesBindingPostProcessor for JavaBean style, ConfigurationPropertiesBinder for constructor binding). For anything beyond a one-off value, use the second.
| Capability | @Value | @ConfigurationProperties |
|---|---|---|
| Granularity | One property per annotation | Whole prefix → one object |
| Relaxed binding (env var ↔ kebab) | Only if you use the canonical kebab-case key | Full |
| Type conversion | Spring ConversionService only | Boot converters: Duration (30s, PT30S), DataSize (10MB), List, Map, nested objects |
| Validation | None | @Validated + Jakarta constraints, fails at startup |
| SpEL | Yes: #{systemProperties['user.home']} | No |
| Immutability | Field/constructor param | Records / constructor binding |
| IDE metadata | None | spring-boot-configuration-processor generates autocompletion + docs |
| Fail on unknown/typo keys | No | Optional (ignoreUnknownFields=false) |
| Best for | A single toggle or label | Any group of related settings |
@Value is still the right tool- Forgetting
@Validated— constraint annotations on the record are inert; a blank URL boots fine and fails later. Also addspring-boot-starter-validation(Hibernate Validator is no longer pulled in by the web starter). - Nested object without
@Valid— constraints onPool.sizeare skipped. Validation does not cascade by itself, same as in controllers. - Mutable JavaBean binding by accident. A class with setters binds through setters; a class with a single parameterised constructor in Boot 3 binds through the constructor. With several constructors annotate the intended one with
@ConstructorBinding— otherwiseNoSuchMethodException/ values left at defaults. - Missing key vs default. A primitive record component with no
@DefaultValuebinds to0/falsesilently. If it must be present, make it a validated wrapper (@NotNull Integer). Silent zero timeouts are a classic outage. @Valueon astaticfield — ignored, field staysnull. Also,@Valuein a class created withnew(not a bean) is never processed.- Placeholder without a default.
@Value("${missing}")→IllegalArgumentException: Could not resolve placeholder 'missing' in value "${missing}"— the right behaviour for required config; add:defaultonly for truly optional values.
Profiles, Groups and @Profile
A profile is a named switch that selects both extra configuration files and extra beans. Two independent mechanisms use it: application-{profile}.yml files layer extra properties on top of the base file (Chapter 03's priority list), and @Profile includes or excludes bean definitions from the context. Activating prod triggers both. If no profile is active, Boot activates the profile named default.
prod,eu: files are layered; later-listed profile wins on conflicts| Goal | Mechanism | Where it may be set |
|---|---|---|
| Choose active profiles | spring.profiles.active | env var, command line, JVM prop, base application.yml — not in a profile-specific file |
| Always add a profile | spring.profiles.include | base config only; invalid in profile-specific docs |
| Expand one profile into several | spring.profiles.group.<name> | base config |
| Add profiles in code | SpringApplication.setAdditionalProfiles("x"), @ActiveProfiles in tests | main method / test class |
| Profile-conditional YAML block | spring.config.activate.on-profile | inside a --- document |
| Profile-conditional bean | @Profile / Environment.acceptsProfiles(Profiles.of("prod")) | any bean/config class |
dev, test, prod, cloud and capability bundles (metrics). For feature toggles prefer a normal property + @ConditionalOnProperty (Chapter 08): toggles can then be changed per deployment without inventing prod-without-recommendations profiles.
- Profile explosion (
prod-eu-blue-canary) — every combination is untested code. Keep profiles few; push the rest into plain properties. @Profile("!prod")as the "default" bean and noprodbean defined →NoSuchBeanDefinitionException: No qualifying bean of type 'Mailer'in prod only. Make sure every profile combination yields exactly one candidate.- Tests silently inheriting the environment. A CI job that exports
SPRING_PROFILES_ACTIVE=prodfor a later deploy step also reaches every@SpringBootTestthat does not pin its profiles. Declare@ActiveProfiles("test")explicitly. - A secret committed in
application-prod.ymlbecause "it's only the prod profile". The jar contains it, so anyone who can download the artifact has production credentials. - A typo in the profile name (
--spring.profiles.active=prd) is not an error: Boot starts with only the base config and a single log lineThe following 1 profile is active: "prd". Check that line on every deploy, or add a startup check that rejects unknown profiles.
Secrets and Per-Environment Configuration
Secrets are configuration with a blast radius. The rules are short: never in git, never in the image, never in a log line, never in an actuator response. Boot has no secrets store of its own — it only needs the value to arrive as a property from somewhere that is access-controlled and auditable.
| Environment | How the secret arrives | How Spring reads it | Rotation |
|---|---|---|---|
| Laptop | Gitignored application-local.yml or .env | spring.config.import=optional:file:.env[.properties] | Manual |
| CI | Masked pipeline variables | Env vars | Per run |
| Kubernetes | Secret mounted as env var or as files | SPRING_DATASOURCE_PASSWORD or configtree:/etc/secrets/ | Restart pods (env) / file change (mount) |
| AWS | Secrets Manager / Parameter Store | Spring Cloud AWS spring.config.import=aws-secretsmanager:... or injected by ECS/EKS | Native rotation + restart |
| HashiCorp | Vault KV / dynamic DB creds | Spring Cloud Vault spring.config.import=vault:// | Short-lived leases |
With a config tree every file's name is the property key and its content is the value, so a Secret key payment.api-key becomes the property payment.api-key. No newline handling is needed — Boot trims a trailing newline. Files are also easier to rotate than env vars because a mounted Secret is updated in place (the app must re-read it; Boot does not reload on its own).
Keys whose names contain password, secret, key, token, credentials or vcap_services were sanitised in Boot 2; Boot 3 flipped to masking everything unless you opt in, because name-based guessing missed things like spring.datasource.url=jdbc:postgresql://user:pw@host.
- Secrets in the Docker image via
COPY application-prod.yml.docker historyor any registry reader extracts them. Mount at runtime instead. - Printing the config at startup (
log.info("props={}", props)on a record withapiKey) → the key lands in the log aggregator. OverridetoString()or mark fields***. - Secret rotation without restart assumption. Rotating the DB password in Vault does nothing for a HikariCP pool holding old connections until it is recreated; plan for rolling restarts or dynamic-lease support.
- Defaults for secrets (
${DB_PASSWORD:changeme}). The default is what runs in the environment where somebody forgot to set it — usually production. For secrets, no default: let the placeholder failure stop the boot. - Committing
.env. Add it to.gitignoreon day one, and rotate anything that was ever pushed — deleting the file does not remove it from history.
@ConditionalOnProperty and Feature Flags
Configuration can decide which beans exist. @ConditionalOnProperty evaluates while the context is being built (the same mechanism auto-configuration uses — see Boot Internals), so a disabled feature has no bean, no scheduler thread, no endpoint and no cost.
| Attribute | Meaning | Gotcha |
|---|---|---|
name / value | Property name(s) (relative to prefix); all must match | Use kebab-case canonical names |
havingValue | Required value (case-insensitive) | If omitted, matches any value that is not false — so enabled=off still counts as enabled |
matchIfMissing | Whether an absent property matches | Default false: with no property the condition fails |
prefix | Common prefix; a dot is added for you | prefix = "a.b." is an error-prone double dot |
| Mechanism | Decided when | Changeable without restart? | Use for |
|---|---|---|---|
@ConditionalOnProperty | Context refresh (startup) | No | Turning whole subsystems on/off per deployment |
@Profile | Context refresh | No | Environment kinds |
Plain if (props.enabled()) | Per call | Only with refreshable config | Cheap branching inside a service |
Spring Cloud @RefreshScope | Per refresh (/actuator/refresh or bus event) | Yes | Rarely: rebuilds the bean; in-flight state is lost |
| External flag service (Unleash, LaunchDarkly, ...) | Per request | Yes, and per user | Gradual rollouts, kill switches |
- Typo in the property name — the condition silently fails and the feature is absent. Verify with
--debugor/actuator/conditions, which lists "Did not match: @ConditionalOnProperty (features.recomendations.enabled=true) did not find property 'enabled'". - Two
@ConditionalOnPropertyconfigs that can both match (nohavingValueon either) →NoUniqueBeanDefinitionException. Make the pair mutually exclusive as above. - Expecting runtime toggling. Changing the property in a ConfigMap does nothing until the pod restarts; the bean was decided at startup.
A Mini Environment and Binder in Plain Java
The best test of understanding the Environment is to build one. The program below is plain Java 17 — no Spring on the classpath — and implements the four ideas from this page: an ordered stack of property sources with first-match-wins, relaxed key matching (PAYMENT_MAXRETRIES ≡ payment.max-retries), placeholder resolution with defaults and cycle detection, and constructor binding of a record with type conversion and fail-fast error aggregation.
| Output line | What it proves |
|---|---|
server.port = 9090 <- commandLineArgs | The command line beat env, application-prod.yml (9000) and the base file (8080): first match down the stack wins. |
payment.max-retries = 5 <- systemEnvironment | Relaxed matching: env var PAYMENT_MAXRETRIES overrode the file key payment.max-retries after both were canonicalised. |
payment.sandbox = false <- application-prod.yml | The profile file sits above the base file, so prod flips true to false. |
payment.url = https://pay.example.com/v1 | The placeholder ${payment.host} is resolved against the whole stack at read time, so a value from the prod file fills a slot defined in the base file. |
log.file = orders-9090.log | A placeholder sees the final winner of server.port (9090), not the base-file value. |
| Binding failure lists both errors together | Aggregating errors (as Boot does) lets you fix all problems in one redeploy instead of one per restart. |
- Origins: each property remembers its file, line and column (the
Origin:line in error messages). - A richer
ConversionService(Duration, DataSize, enums, nested beans,Map/Listindices). - Lazy, per-source
ConfigurationPropertyNameadapters instead of re-canonicalising at load time. - Config data lifecycle: imports,
optional:, profile expansion, activation conditions.
Diagnosing "Why Is This Value Wrong?"
When a value is "wrong", resist the urge to grep the repository. The question is always which source won, and the application can tell you.
| Symptom | First check | Typical root cause |
|---|---|---|
| Service uses the wrong DB / URL | GET /actuator/env/spring.datasource.url → propertySources array lists every source defining the key, top = winner | An env var or leftover ./config/application.yml shadows the file you edited |
| Feature is off in one environment | --debug or /actuator/conditions → "Did not match" section | Typo in a @ConditionalOnProperty key, or the profile that sets it is not active |
| Which profiles are active? | Startup log: The following 2 profiles are active: "prod", "eu"; /actuator/env → activeProfiles | Env var not delivered into the container |
A @ConfigurationProperties field is null/0 | /actuator/configprops shows the bound object | Wrong prefix, or @EnableConfigurationProperties / scanning missing (a kebab/camel mismatch is not the usual cause: relaxed binding covers it) |
| Placeholder explodes | Exception text names the key: Could not resolve placeholder 'x' in value "${x}" | Key defined only in a profile that is not active; no :default |
| Config file seems ignored | Run with --debug and set logging.level.org.springframework.boot.context.config=TRACE | File in the wrong location, wrong name, or not in the built jar (check with unzip -l) |
Expose env and configprops only on the management port or behind authentication. Even with values masked as ****** you still get the source name and origin, which is usually all you need to find the culprit.
Questions You Will Be Asked
Short answers first; the trap is the follow-up an interviewer uses to test whether you have operated this, not just read about it.
Command-line args > JVM system props > OS env vars > profile-specific files > base files > @PropertySource > defaults. Per key, first match wins; nothing merges. Trap: lists are replaced entirely, maps merge by key.
@Value or @ConfigurationProperties?
@ConfigurationProperties for groups: relaxed binding, rich conversion, validation, immutability via records, IDE metadata. @Value for a single value or SpEL. Trap: prefer canonical kebab-case keys in @Value so env-var overrides still match.
PAYMENT_MAXRETRIES map to payment.max-retries?
Both are canonicalised: lower-case, underscores to dots (env only), dashes removed. The Binder compares canonical forms. Trap: an env var cannot contain a dash, so the dash is dropped, not converted.
Constraint annotations + @Validated on the properties class (and @Valid on nested types), or a placeholder with no default. Trap: without spring-boot-starter-validation on the classpath the annotations do nothing.
spring.profiles.active, include and group?
active chooses the set; include adds always-on profiles; group expands one name into several. Trap: active and include are invalid inside a profile-specific file.
spring.config.activate.on-profile per --- document; later documents override earlier. Trap: the old spring.profiles key no longer works in Boot 3.
Kubernetes Secret (or an external manager) mounted as env var or files + configtree:; never in the image or repo. Trap: defaults like ${DB_PASSWORD:changeme} mask a missing secret.
Not by default: Boot reads config once. Options: Spring Cloud @RefreshScope + /actuator/refresh, an external flag service, or re-reading files yourself. Trap: @RefreshScope rebuilds the bean and loses in-flight state.
/actuator/env/<key> lists sources in priority order with origins; /actuator/configprops shows bound objects. Trap: Boot 3 masks values, but source and origin remain.
matchIfMissing do?
Makes @ConditionalOnProperty match when the property is absent. Trap: without havingValue, anything except literal false matches.
Summary & Cheat Sheet
| Need | Use | Remember |
|---|---|---|
| Override one value for a run | Command-line --k=v | Highest of the normal sources |
| Containers / Kubernetes | Env vars UPPER_SNAKE | Dashes dropped; dots to underscores |
| Defaults shipped in the build | application.yml in the jar | Lowest real file; keep secrets out |
| Per-environment values | application-<profile>.yml / multi-doc | Profile file overrides base per key |
| A group of related settings | @ConfigurationProperties record + @Validated | Fail fast at startup |
| A single value / SpEL | @Value | Use the canonical kebab key |
| Secrets | K8s Secret / Vault / ASM + configtree: | No defaults, no logs, masked actuator |
| Turn a subsystem on/off | @ConditionalOnProperty | Decided at startup only |
| Why is the value wrong? | /actuator/env/<key>, --debug | First match wins; check origin |
- The Environment is a priority stack of PropertySources; lookup is per key, first match wins, lists are replaced not merged.
- Closer to the process wins: command line > env vars > profile files > base files > defaults.
- Relaxed binding: write kebab-case in files, UPPER_SNAKE in env vars; both map to the same canonical key.
- @ConfigurationProperties + records + @Validated makes bad config a startup failure naming the exact property, value and origin.
- Profiles select files and beans; keep them few; activate with an env var; groups bundle concerns.
- Secrets arrive from the platform, never from the repo, image or logs; actuator masks values by default in Boot 3.
- Config can wire beans (
@ConditionalOnProperty), but only at startup.
- Boot Internals — the
@Conditionalmachinery behind@ConditionalOnProperty, and how--debugprints it. - Actuator & Observability —
/env,/configprops,/loggersand how to expose them safely. - Testing —
@TestPropertySource,@ActiveProfiles,@DynamicPropertySource. - Security — protecting management endpoints and keeping secrets out of responses.
- JWT, OAuth2 & OIDC — issuer URIs and client secrets as configuration.