@ConfigurationProperties
Bind a whole prefix of settings to one typed object, register it, and get conversion, relaxed binding and IDE completion for free.
@ConfigurationProperties("app.mail")
class MailSettings { getters + setters }
@SpringBootApplication
@ConfigurationPropertiesScan
class App { }@ConfigurationProperties binds every key under a prefix to the fields of one class. Instead of five @Value strings scattered across services, you get one object such as MailSettings that the rest of the code injects like any other bean.
Binding a class
With a JavaBean-style class, Boot creates the object with its no-argument constructor and calls setters. A key app.mail.from-address binds to setFromAddress; kebab-case, camelCase and underscores all match, which is called relaxed binding. Nested objects and collections bind as well: app.mail.retry.max-attempts fills getRetry().setMaxAttempts(...), and app.mail.bcc[0] fills a list.
Registering the class
The annotation on its own does nothing. The class must be registered, in one of three ways:
@ConfigurationPropertiesScanon the main class, which finds every annotated class in that package and below. This is the simplest for applications.@EnableConfigurationProperties(MailSettings.class)on a configuration class, which is explicit and is what libraries use.@ConfigurationProperties("app.datasource")on a@Beanmethod, which binds properties onto an object you do not own, such as a connection pool.
Do not also put @Component on the class. It works, but mixing component scanning with property binding makes it harder to see where the bean comes from.
Why it beats @Value
- One rename in one place, not a search across the codebase.
- Types are checked when the application starts:
Duration,DataSize(10MB), enums, lists and maps all convert, and a bad value stops startup with the key name in the message. - Validation with
@Validated(covered later in this category). - With the
spring-boot-configuration-processordependency, your IDE completes your ownapp.*keys and shows the Javadoc of each field.
Mistakes
A missing setter on a simple field is not ignored: when the key is set, start-up fails with Failed to bind properties under 'app.mail', caused by No setter found for property: from-address. A nested object only needs a getter, because Boot binds onto the instance the class already created. A typo in the YAML (from-adress) is also ignored by default, because unknown keys under a prefix are allowed. Keep property classes free of logic and of mutable state shared between requests: they are singletons and every thread reads the same instance. If you want them immutable, use a record, which the next lesson covers and which is the better default for new code.
Use a prefix you own (app. or your company name). Putting your keys under spring. risks a clash with a future Boot property.
Example
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.stereotype.Service;
// registers every @ConfigurationProperties class below this package
public class NotificationsApplication {
public static void main(String[] args) {
SpringApplication.run(NotificationsApplication.class, args);
}
}
("app.mail")
class MailSettings {
/** Address used in the From header of every outgoing message. */
private String fromAddress = "[email protected]";
private List<String> bcc = new ArrayList<>();
private final Retry retry = new Retry();
public String getFromAddress() { return fromAddress; }
public void setFromAddress(String fromAddress) { this.fromAddress = fromAddress; }
public List<String> getBcc() { return bcc; }
public void setBcc(List<String> bcc) { this.bcc = bcc; }
public Retry getRetry() { return retry; }
static class Retry {
private int maxAttempts = 3;
private Duration backoff = Duration.ofSeconds(2);
public int getMaxAttempts() { return maxAttempts; }
public void setMaxAttempts(int maxAttempts) { this.maxAttempts = maxAttempts; }
public Duration getBackoff() { return backoff; }
public void setBackoff(Duration backoff) { this.backoff = backoff; }
}
}
class WelcomeMailer {
private final MailSettings settings;
WelcomeMailer(MailSettings settings) {
this.settings = settings;
}
String describe() {
return "From " + settings.getFromAddress() + ", up to "
+ settings.getRetry().getMaxAttempts() + " attempts";
}
}When to use it
- Grouping the SMTP sender, BCC list and retry policy of a notifications service into one typed MailSettings bean.
- Configuring a second Hikari connection pool for a read-only reporting database from app.reporting.datasource.* keys.
- Giving the team IDE completion and type checking for twenty custom app.* keys that were previously scattered @Value strings.
More examples
The YAML that binds to MailSettings
`from-address` binds to `fromAddress`, the YAML list fills `bcc`, and the nested `retry` block binds onto the `Retry` object the class already created (it has a getter and no setter, which is enough for a nested object). `500ms` converts to a `Duration`. Any field you leave out keeps the default assigned in the class.
app:
mail:
from-address: [email protected]
bcc:
- [email protected]
- [email protected]
retry:
max-attempts: 5
backoff: 500msBinding onto a class you do not own
Putting the annotation on a `@Bean` method binds the prefix onto whatever object the method returns, using that class's setters. This is how you configure a second connection pool for a read replica or reporting database. Note that Hikari's setter is `setJdbcUrl`, so the key is `jdbc-url`, not `url`. Defining your own `DataSource` bean also switches off Boot's auto-configured one, so declare the primary pool too if you still need it.
import javax.sql.DataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class ReportingDataSourceConfig {
// app.reporting.datasource.jdbc-url, .username, .password, .maximum-pool-size ...
// are bound straight onto HikariDataSource's own setters.
@Bean
@ConfigurationProperties("app.reporting.datasource")
DataSource reportingDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
}IDE completion for your own keys
The annotation processor reads your `@ConfigurationProperties` classes during compilation and writes metadata that IntelliJ IDEA, VS Code and Eclipse use to complete `app.mail.*` keys, show their types and defaults, and flag unknown keys. Field Javadoc becomes the description. It adds nothing at runtime. From JDK 23, javac no longer runs annotation processors it merely finds on the class path, so unless your build already enables them, also list it under the Maven compiler plugin's `annotationProcessorPaths` (or Gradle's `annotationProcessor` configuration).
<!-- pom.xml: generates META-INF/spring-configuration-metadata.json at compile time -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
Discussion