[← Registration](03-registration.md) · [Index](../README.md) · [Validation →](05-validation.md) # 4. Records, constructor binding and defaults See [`MailProperties`](../src/main/java/com/ankurm/configprops/props/MailProperties.java) and the bound output in [`03-value-vs-binding.txt`](output/03-value-vs-binding.txt). ## Records need no annotation A record has exactly one canonical constructor, so the binder uses it. `@ConstructorBinding` is only needed to pick between candidates when a type has more than one constructor — and since Spring Boot 3 it goes on the *constructor*, not the type. Constructor binding gives you immutability, which matters more than it sounds: a mutable `@ConfigurationProperties` bean is a singleton that anything can write to. ## Defaults A record cannot have field initialisers, so the default has to be attached to the component: ```java public record MailProperties( String host, // no default: null if absent @DefaultValue("587") int port, @DefaultValue("30s") Duration timeout, @DefaultValue RetryProperties retries, // nested, with its own defaults @DefaultValue List recipients, // empty list, not null @DefaultValue Map headers) { } ``` `@DefaultValue` with no argument on a nested type means "construct it with its own defaults" rather than "bind it to null". Same for collections: you get an empty one instead of `null`, which removes a class of `NullPointerException` from startup code. A component with neither a value nor a `@DefaultValue` binds to `null` for a reference type. A `record` component of primitive type with no value and no default fails the bind. ## The whole-object rule If *nothing* under the prefix is present, the binder returns no result at all — not an object full of defaults. `BindResult.isBound()` is `false` and `get()` throws. Defaults apply to components of an object that is being constructed; they do not cause one to be constructed. For a bean registered through `@ConfigurationPropertiesScan` this is invisible, because Spring Boot binds with a target that always constructs. It shows up as soon as you call `Binder` yourself, which is why it is here. ## Lists | Written as | `@ConfigurationProperties` | `@Value` | |---|---|---| | YAML block list | binds | does not see it | | `a,b,c` string | binds | binds | | `foo[0]`, `foo[1]` | binds | does not see it | The middle row is the only shape both understand, and it is why comma-separated lists persist in configuration long after they stopped being pleasant to read. One thing to know about list overriding: a list is bound from the highest-precedence source that contains the property, *entirely*. Lists do not merge across sources. A source that sets three elements replaces a lower source's two; it does not append to them.