The full picture
@ConfigurationProperties(prefix = "acme.payment")
@Validated
public record PaymentProperties(
@NotNull URI endpoint,
@DurationUnit(ChronoUnit.SECONDS) @DefaultValue("5s") Duration timeout,
@Min(1) @Max(64) @DefaultValue("8") int maxConcurrent,
List<@NotBlank String> allowedCurrencies,
Retry retry
) {
public record Retry(@DefaultValue("3") int maxAttempts,
@DefaultValue("200ms") Duration backoff) {}
}
// + @EnableConfigurationProperties(PaymentProperties.class) or @ConfigurationPropertiesScan
// Bad config -> BindValidationException AT STARTUP with the exact property path.
// vs @Value("${acme.payment.timeout}") long timeoutMs; // stringly, scattered, unvalidated- Startup validation is the killer feature: a typo'd duration fails the deploy, not the 3am code path that first reads it.
- Records/constructor binding give immutable config — no mutable singleton settings drifting at runtime.
- spring-boot-configuration-processor generates metadata → IDE autocompletion and docs for your own properties; platform teams should treat that as mandatory.
- Testing: bind in isolation with
new Binder(source).bind("acme.payment", …)or context-runner tests — config bugs get unit tests like code bugs. @Valueniche kept honest: single values, SpEL (#{systemProperties…}), places where a whole properties class is ceremony.