How does constructor binding work and how does it enable immutable @ConfigurationProperties?
answer
- final fields, immutable
- SB3: single parameterized ctor => automatic
- @ConstructorBinding only picks a ctor
- must NOT be @Component
- @DefaultValue + records
basics
~20 sBind values through the constructor instead of setters, so fields can be final and the config object is immutable. In Spring Boot 3 a single non-default constructor triggers constructor binding automatically; you no longer need setters.
solid answer
~40 sConstructor binding populates a @ConfigurationProperties bean via its constructor parameters rather than setters, letting you make fields final and the whole object immutable. In Spring Boot 2.2+ you marked it with @ConstructorBinding; in Spring Boot 3 the annotation on the type is no longer needed — if the class has a single, parameterized (non-default) constructor, Spring uses it automatically (you only add @ConstructorBinding on a specific constructor when there are several). You register the class the same way (@EnableConfigurationProperties or @ConfigurationPropertiesScan); crucially, constructor-bound classes must NOT be @Component beans. Defaults for missing values come from @DefaultValue on parameters. This pairs perfectly with Java records and Kotlin data classes, giving concise, immutable, thread-safe config. Nested types bind recursively, each using its own constructor. Note @Value cannot be used on constructor-bound parameters.
code
java · 16 lines@ConfigurationProperties(prefix = "app.pool")
public record PoolProperties(
@DefaultValue("10") int maxSize,
@DefaultValue("true") boolean fair,
Duration acquireTimeout,
Ssl ssl) {
public record Ssl(@DefaultValue("false") boolean enabled, String keyStore) {}
}
@Configuration
@EnableConfigurationProperties(PoolProperties.class)
class PoolConfig {
// In Spring Boot 3, no @ConstructorBinding needed:
// the record's canonical constructor is used automatically.
}go deeper
Know it enables immutable/final-field config, often via records.
Explain @ConstructorBinding history and @DefaultValue for optional values.
Detail the SB3 single-constructor rule, the no-@Component constraint, and records/Kotlin fit.
Mandate immutable constructor-bound config as the standard; reason about thread-safety and config as an immutable contract.
## Setter binding vs constructor binding Classic `@ConfigurationProperties` uses **JavaBean (setter) binding**: Spring instantiates the object with a no-arg constructor and calls setters. That forces mutable fields. **Constructor binding** instead resolves each *constructor parameter* from configuration and calls the constructor once, letting you declare `final` fields and an immutable object. ## History / how to trigger it - **Spring Boot 2.2–2.x**: annotate the class (or constructor) with `@ConstructorBinding` (package `org.springframework.boot.context.properties`). - **Spring Boot 3.x**: `@ConstructorBinding` at the **type level is no longer needed**. If a class has a **single non-default (parameterized) constructor**, Spring performs constructor binding automatically. You place `@ConstructorBinding` on a **specific constructor** only to disambiguate when the class has more than one constructor. ## Registration constraint Constructor-bound `@ConfigurationProperties` classes **cannot be regular Spring beans** (no `@Component`, no `@Bean` returning them). Register them with `@EnableConfigurationProperties(Xxx.class)` or `@ConfigurationPropertiesScan`. Mixing constructor binding with `@Component` throws an error. ## Defaults and optionality Because there are no setters to leave a default value, use `@DefaultValue` on the parameter: ```java @ConfigurationProperties(prefix = "app.pool") public record PoolProps( @DefaultValue("10") int maxSize, @DefaultValue("true") boolean fair, Duration timeout) {} ``` Missing `app.pool.max-size` yields `10`. A missing value with no `@DefaultValue` binds to `null` (or the primitive's Spring-supplied default). Nested objects can also use `@DefaultValue` to be constructed even when absent. ## Records and Kotlin - **Java records**: ideal — the canonical constructor becomes the binding constructor; fields are final. - **Kotlin data classes**: work well; default parameter values act like `@DefaultValue`. ## Nested binding Nested immutable types bind recursively, each via its own constructor. Lists/maps of such types are supported. ## Gotchas - Forgetting the class can't be `@Component`. - Adding a second constructor without `@ConstructorBinding` on one -> ambiguity error. - Expecting `@Autowired` into a constructor-bound object — the constructor is for config binding, not DI of other beans; don't mix. - `@Value` is **not** supported on constructor-bound parameters. - Relaxed binding and validation (`@Validated`) still apply. ## When to use Prefer constructor binding for new config classes: immutability, thread-safety, no accidental late mutation, and concise records.
- In Spring Boot 3, when do you still need @ConstructorBinding?Only to select a specific constructor when the class has more than one constructor. With a single parameterized constructor it's inferred automatically.
- Why can't a constructor-bound @ConfigurationProperties class also be a @Component?Constructor binding and normal bean instantiation conflict; Spring requires such classes to be registered via @EnableConfigurationProperties or @ConfigurationPropertiesScan, and rejects @Component on them.
- How do you give a constructor-bound property a default value?Annotate the constructor parameter with @DefaultValue("..."), or for Kotlin use a default parameter value.
saying these in an interview costs you the question
- Saying you still must annotate the type with @ConstructorBinding in Spring Boot 3
- Putting @Component on a constructor-bound class
- Claiming @Value works on constructor-bound parameters
- Thinking constructor binding is for injecting other beans (DI) rather than config values