Spring Boot @ConfigurationProperties Example
Spring Boot 11 min read
Bind external configuration to typed objects: records with constructor binding, relaxed binding rules, validation that fails at startup instead of at 3am, and when @Value is still the right tool.
@Value("${some.property}") works, scattered across the classes that need it, until the day
someone deploys with some.propety misspelled and the failure arrives as a NullPointerException
in a scheduled job at three in the morning.
@ConfigurationProperties binds a whole group of properties to one typed object, once, and can
refuse to start the application if the configuration is wrong. That last part is the reason to use
it.
Written against Spring Boot 3.2 and Java 17.
A record is the best shape for this
Spring Boot 3 binds through a constructor when the type has a single one, and a record has exactly
one. No setters, no mutable state, no @ConstructorBinding annotation needed:
package com.example.notes.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
import java.util.List;
@ConfigurationProperties(prefix = "notes.storage")
public record StorageProperties(
String location,
Duration retention,
List<String> allowedExtensions,
Limits limits) {
public record Limits(long maxFileSizeBytes, int maxFilesPerUser) { }
}
notes:
storage:
location: /var/lib/notes
retention: 30d
allowed-extensions: [pdf, png, txt]
limits:
max-file-size-bytes: 10485760
max-files-per-user: 200
Nested records bind to nested YAML. Duration accepts 30d, 45s, PT10M; DataSize accepts
10MB. Both are Spring conversions, not Java ones. A String field would just hold the text.
Register it
The type has to be picked up. Three ways, in order of preference:
// 1. scan a package — the usual choice
@SpringBootApplication
@ConfigurationPropertiesScan("com.example.notes.config")
public class NotesApplication { }
// 2. name it explicitly
@Configuration
@EnableConfigurationProperties(StorageProperties.class)
class StorageConfig { }
// 3. annotate the class as a component — only works with setter binding
@Component
@ConfigurationProperties(prefix = "notes.storage")
class StorageProperties { ... }
The third does not work for records or any constructor-bound type; Spring needs to construct the
object itself rather than receive it from the container. Mixing @Component with constructor
binding is a common and confusing failure.
Then inject it like any bean:
@Service
public class StorageService {
private final StorageProperties props;
public StorageService(StorageProperties props) {
this.props = props;
}
public Path resolve(String filename) {
return Path.of(props.location()).resolve(filename).normalize();
}
}
Relaxed binding
One property can be written several ways and they all bind to the same field:
notes.storage.max-file-size-bytes <- kebab-case, the canonical form
notes.storage.maxFileSizeBytes <- camelCase
notes.storage.max_file_size_bytes <- underscores
NOTES_STORAGE_MAXFILESIZEBYTES <- environment variable
That last form is the point. Environment variables cannot contain dots or dashes, so relaxed
binding is what makes @ConfigurationProperties work with container configuration and secret
managers without a translation layer.
Use kebab-case in your own files. It is the documented canonical form, and it is what the configuration metadata generator emits.
@Value does not do this. @Value("${notes.storage.maxFileSizeBytes}") will not resolve from
max-file-size-bytes. Another reason the two are not interchangeable.
Validation, at startup
This is the feature that pays for itself:
@ConfigurationProperties(prefix = "notes.storage")
@Validated
public record StorageProperties(
@NotBlank String location,
@NotNull @DurationMin(days = 1) Duration retention,
@NotEmpty List<String> allowedExtensions,
@Valid Limits limits) {
public record Limits(
@Positive long maxFileSizeBytes,
@Min(1) @Max(10_000) int maxFilesPerUser) { }
}
Needs spring-boot-starter-validation on the classpath, since Spring Boot 2.3 it is not
transitive. With it, a missing or invalid value fails the context:
Binding to target StorageProperties failed:
Property: notes.storage.location
Value: null
Reason: must not be blank
The application does not start. That is the correct outcome: a misconfigured deployment that refuses to boot is caught by the rollout, while one that starts and misbehaves is caught by a user.
@Valid on the nested field is required, without it the nested record’s constraints are not
evaluated.
Profiles
Put the shape in the main file and the environment differences in profile files:
# application.yaml
notes:
storage:
location: /tmp/notes
retention: 7d
# application-prod.yaml
notes:
storage:
location: /var/lib/notes
retention: 365d
Profile files override, key by key, rather than replacing the block: allowed-extensions set only
in the main file still applies under prod.
In a single file, use activation blocks rather than the removed spring.profiles:
notes.storage.retention: 7d
---
spring.config.activate.on-profile: prod
notes.storage.retention: 365d
Activate with SPRING_PROFILES_ACTIVE=prod, --spring.profiles.active=prod, or
@ActiveProfiles("prod") in a test.
A list does not merge. If application.yaml defines three extensions and application-prod.yaml
defines one, production has one. This surprises people every time; there is no append.
@Value is still fine sometimes
@ConfigurationProperties is right for a group of related settings that belong to a component.
@Value is right for one value used in one place:
@Value("${notes.storage.location}")
private String location; // fine for a single value
@Value("${notes.retention:30d}")
private Duration retention; // with a default after the colon
What @Value cannot do: relaxed binding, validation, nested types, collections beyond simple
comma-splitting, or IDE completion. It also spreads knowledge of your configuration keys across
every class that reads one, so a rename becomes a search.
Binding a type you do not own
@ConfigurationProperties also works on a @Bean factory method, which is how you configure a
third-party class from your own property namespace without wrapping it:
@Configuration
class ClientConfig {
@Bean
@ConfigurationProperties(prefix = "notes.upstream")
ThirdPartyClientOptions upstreamOptions() {
return new ThirdPartyClientOptions();
}
}
Spring creates the object, then binds notes.upstream.* onto its setters. This is setter binding by
necessity, you cannot add a constructor to a class you did not write, so the type needs mutable
properties, which most configuration-holder classes have.
It is also the cleanest way to expose a library’s settings under names that match the rest of your configuration, rather than leaking the library’s own vocabulary into every environment file. If the library later renames a setter, the failure appears at startup rather than as a silently unset value.
Metadata for your editor
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
This annotation processor writes META-INF/spring-configuration-metadata.json at compile time, and
IDEs read it to autocomplete your properties in application.yaml and flag unknown keys. Javadoc
on the record components becomes the hover documentation. It costs one optional dependency and
removes the most common configuration error, a typo, from the editor rather than from runtime.
Frequently asked questions
Why are my properties all null?
The type was never registered. Add
@ConfigurationPropertiesScan to the application class, or @EnableConfigurationProperties(X.class)
on a configuration class.
Can I use a record?
Yes, and it is the preferred shape on Spring Boot 3, a single constructor means constructor binding is used automatically, with no annotation.
Why does @Component not work with my record?
@Component makes the container instantiate the
bean, but constructor binding requires Spring to construct it from resolved properties. Use
@ConfigurationPropertiesScan or @EnableConfigurationProperties instead.
Do I still need @ConstructorBinding?
Only when a class has more than one constructor and you must indicate which to bind. With a single constructor, including every record. It is implicit.
How do environment variables map to properties?
Upper-case with underscores for separators:
notes.storage.location becomes NOTES_STORAGE_LOCATION. Relaxed binding is what makes this work.
Why is validation not running?
Two likely causes: @Validated is missing from the class, or
spring-boot-starter-validation is not on the classpath, it stopped being transitive in Spring
Boot 2.3. For nested types, @Valid on the field is also required.
Do lists merge across profiles?
No. A list defined in a profile file replaces the base list entirely. There is no append semantics for collections.
What is the difference between @Value and @ConfigurationProperties?
@Value reads one key with
exact spelling and no validation. @ConfigurationProperties binds a group with relaxed binding,
type conversion, nested objects and startup validation.
Can I bind a Map?
Yes: Map<String, String> binds from notes.labels.env=prod style keys, and
Map<String, SomeRecord> binds nested blocks. Useful for a bounded set of named configurations.
How do I see the resolved values at runtime?
Actuator’s /actuator/configprops reports bound
properties. Keep it off any public port, see
securing Actuator endpoints.
Where should I go next?
The Spring Boot REST API guide uses this pattern for datasource and upload settings, and the other Spring Boot guides build on the same application.