skip to content

How does constructor binding work and how does it enable immutable @ConfigurationProperties?

level: seniorimportance: should knowfreq 55%

answer

  1. final fields, immutable
  2. SB3: single parameterized ctor => automatic
  3. @ConstructorBinding only picks a ctor
  4. must NOT be @Component
  5. @DefaultValue + records

basics

~20 s

Bind 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 s

Constructor 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
java
@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

for a junior

Know it enables immutable/final-field config, often via records.

for a middle

Explain @ConstructorBinding history and @DefaultValue for optional values.

for a senior

Detail the SB3 single-constructor rule, the no-@Component constraint, and records/Kotlin fit.

for a principal

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

context