Configuration: application.yml, Profiles & @ConfigurationProperties¶
The same jar should run on your laptop, in CI, and in production. What changes between
them — database URLs, credentials, feature flags, timeouts — belongs in configuration, not
code. Spring Boot merges many configuration sources into one Environment and lets you
bind it to typed objects.
application.yml¶
spring:
application:
name: tasks
server:
port: 8080
shutdown: graceful
tasks:
max-open-tasks: 100
default-page-size: 20
notifications:
enabled: false
from-address: noreply@example.com
Keys are hierarchical: tasks.notifications.enabled is one property. Boot's own
properties (server.port, spring.datasource.url, logging.level.*) are documented in
the "Common Application Properties" appendix of the reference docs, and IDEs autocomplete
them.
Where values come from, and who wins¶
Boot reads many sources. Higher in this list overrides lower (simplified to the ones you will actually use):
- Command-line arguments:
java -jar app.jar --server.port=9090 SPRING_APPLICATION_JSONenvironment variable- OS environment variables:
SERVER_PORT=9090 - Profile-specific files:
application-prod.yml application.ymlpackaged in the jar- Defaults in
@ConfigurationPropertiesclasses
Environment variables use relaxed binding. The documented rule for turning a
property name into a variable name is: replace dots with underscores, remove dashes, and
uppercase. So tasks.max-open-tasks becomes TASKS_MAXOPENTASKS, and
spring.datasource.url becomes SPRING_DATASOURCE_URL. (Underscores in an env var always
mean "a dot", so TASKS_MAX_OPEN_TASKS would be read as tasks.max.open.tasks — a
different, nested key.)
Files are also loaded from outside the jar — a config/ directory next to where you run
it, or anywhere you point spring.config.import / spring.config.additional-location at
— and those override the packaged ones. This is how you add production settings without
rebuilding.
Profiles¶
A profile is a named set of configuration that is only active when you ask for it:
# application.yml — shared defaults
tasks:
max-open-tasks: 100
logging:
level:
com.example.tasks: INFO
---
spring:
config:
activate:
on-profile: dev
logging:
level:
com.example.tasks: DEBUG
---
spring:
config:
activate:
on-profile: prod
server:
shutdown: graceful
The --- separators create multiple documents in one file; each later document
overrides the earlier ones when its condition matches. The alternative is separate
application-dev.yml / application-prod.yml files, which is easier to read once a
profile grows beyond a few lines.
Activate profiles with --spring.profiles.active=dev, the SPRING_PROFILES_ACTIVE
environment variable, or @ActiveProfiles("test") in tests. Beans can be profile-specific
with @Profile("dev").
Use profiles for environments or modes, not for every tweak. If you find yourself with
prod-eu-large-cache, you want properties, not profiles.
@ConfigurationProperties: typed, validated config¶
Injecting single values with @Value("${tasks.max-open-tasks}") works but scatters string
keys across the codebase and gives no validation. Bind a whole prefix to a record instead:
package com.example.tasks.task;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.validation.annotation.Validated;
@Validated
@ConfigurationProperties(prefix = "tasks")
public record TaskProperties(
@Min(1) @Max(10_000) int maxOpenTasks,
@DefaultValue("20") int defaultPageSize,
Notifications notifications) {
public record Notifications(boolean enabled, @NotBlank String fromAddress) { }
}
Register it by adding @ConfigurationPropertiesScan to the main class (or
@EnableConfigurationProperties(TaskProperties.class) on a configuration class), then
inject it like any bean:
@Service
class TaskService {
private final TaskProperties props;
TaskService(TaskProperties props) { this.props = props; }
}
Worked example: failing fast on bad config¶
With the record above, start the app with an invalid value:
Startup stops before the web server opens. Running exactly this against the Level 1 project (Boot 4.1) printed:
***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target com.example.tasks.task.TaskProperties failed:
Property: tasks.maxOpenTasks
Value: "0"
Origin: "tasks.max-open-tasks" from property source "commandLineArgs"
Reason: must be greater than or equal to 1
Note the Origin line: Boot tracks which property source supplied each value, which is
invaluable when you cannot tell whether a setting came from a file, an environment
variable, or the command line.
That is precisely what you want: a misconfigured deployment fails immediately and loudly,
instead of misbehaving at 3 a.m. when the first user hits the limit. Add
spring-boot-configuration-processor as an optional dependency and your IDE also gets
autocomplete and documentation for tasks.*.
How It Actually Works¶
The Environment is an ordered list of PropertySources — command-line args, system
properties, env vars, each loaded config file — and a lookup walks them in order,
returning the first hit. Profile-specific documents are inserted ahead of the default ones
when their profile is active, which is how they override.
Binding is done by Boot's Binder. For @ConfigurationProperties it walks the target type:
for a record it finds the canonical constructor and, for each parameter, computes the
property name (maxOpenTasks → tasks.max-open-tasks), looks it up through all property
sources using relaxed-name matching, and converts the string with Spring's
ConversionService — which is how "30s" becomes a Duration, "10MB" a DataSize,
and "a,b,c" a List<String>. Nested records recurse. Afterwards, because the class is
@Validated, the bound object is passed to the Bean Validation Validator; any violation
aborts context startup with the report above.
@Value is resolved differently: a BeanPostProcessor evaluates ${…} placeholders (and
#{…} SpEL expressions) when it injects each field or parameter. It does not support
relaxed binding in the same way and has no validation, which is why
@ConfigurationProperties is preferred for anything beyond a single flag.
Common mistakes¶
- Committing secrets to
application.yml. Passwords and API keys come from environment variables or a secret store (Level 4). The file can contain${DB_PASSWORD}placeholders. - Profile explosion. A profile per customer or per region quickly becomes unreadable.
- Forgetting that tests pick up
application.ymltoo. Usesrc/test/resources/application.ymlor@TestPropertySource/@DynamicPropertySourcefor test overrides. - Mutable config classes with setters that other beans modify at runtime. Records make configuration immutable by construction.
- Typos that bind to nothing. An unknown key such as
tasks.max-opentasksis silently ignored. The configuration processor warns in the IDE;@Validatedwith@Minwould catch the resulting default of0.
Exercise¶
- Create
TaskPropertieswithmaxOpenTasks,defaultPageSize(default 20), and a nestedNotificationsrecord. Validate them. - Add
devandprodprofiles:devlogs your package atDEBUG;prodsetsserver.shutdown: graceful. Start with each profile and verify the difference with/actuator/env(add Actuator) or by logging the injected properties at startup. - Override
tasks.max-open-tasksthree ways — command line, environment variable, and an externalconfig/application.yml— and confirm which wins when all three are set. - Start with an invalid value and paste the startup failure into a comment in your properties class, so the next developer knows what to expect.