Skip to content
Preptima
mediumConceptCodingMidSeniorStaff

Why would you bind configuration with @ConfigurationProperties rather than @Value, and how do you make a bad value fail at startup instead of at the first request?

@Value injects one expression per field with no grouping, no relaxed name matching and no validation. @ConfigurationProperties binds a whole subtree onto a typed object, matches names loosely, handles lists and nested types, and combined with @Validated it fails the context refresh when the environment is wrong.

4 min readUpdated 2026-07-29Target archetype: Big Tech, Enterprise Captive, Product Startup
Practice answering out loud

What the interviewer is scoring

  • Does the candidate name relaxed binding and say which of the two mechanisms actually has it
  • Whether validation is described as failing the context refresh rather than as logging a warning
  • That they can register a properties class more than one way and choose deliberately between them
  • Can they say what happens to a list or a map value under @Value
  • Whether the capability given up by moving off @Value is identified as expression evaluation

Answer

One property at a time versus one bound object

@Value resolves a placeholder expression against the environment and injects the result into a single field or parameter. It has no concept of a group: ten related settings become ten annotations on ten fields, each independently able to be missing, each converted in isolation, and none of them checked against the others. @ConfigurationProperties takes the opposite approach. You declare a type whose shape mirrors a subtree of the configuration, and the binder walks the environment filling it in, including nested objects, lists, maps and enums.

The difference that shows up first is testability. A class holding a bound properties object can be constructed in a unit test by passing one populated object. A class with eight @Value fields can only be exercised through a Spring context or by reflection, which is why those classes tend to acquire integration tests that exist purely to supply configuration.

Relaxed binding belongs to the binder, not to the placeholder

This is the detail interviewers listen for. Relaxed binding is a feature of the @ConfigurationProperties binder: a property canonically named app.retry.max-attempts can be supplied as app.retry.maxAttempts in a properties file, or as APP_RETRY_MAXATTEMPTS in the environment, and the binder will match it. A placeholder in @Value does no such matching. It looks up the name you wrote, exactly, and if the only source is an environment variable in upper snake case the property is simply not found.

That asymmetry is behind an entire category of deployment incident: the setting works locally from a YAML file and is silently absent in the container where configuration arrives as environment variables, so the code falls through to a default nobody chose. Bound properties do not have that failure mode, which is reason enough to prefer them for anything an operator will set.

Constructor binding buys you immutability

Bind through a constructor and the resulting object cannot be mutated after startup, which removes a class of question about what happens if something changes it. In Spring Boot 3.x, a type with a single parameterised constructor is bound through that constructor automatically, so @ConstructorBinding is only needed to disambiguate when there are several; that makes a record a natural fit.

@ConfigurationProperties(prefix = "app.retry")
@Validated
public record RetryProperties(
        @Min(1) @Max(10) int maxAttempts,
        // Parsed from "500ms" or "2s" without a custom converter.
        @NotNull Duration initialBackoff,
        @NotEmpty List<@NotBlank String> retryableStatuses) {
}

Registration is explicit and you should know all three routes: @EnableConfigurationProperties naming the type, @ConfigurationPropertiesScan on the application class to pick up annotated types by package, or making the type a bean in the usual way. For an internal starter, naming the type from your auto-configuration is the right choice, because scanning depends on where the consuming application's packages happen to be.

Failing at refresh rather than at the first request

@Validated on the properties type activates bean validation against the constraints, and it runs during binding, which happens while the context is refreshing. An invalid value therefore prevents the application from starting, and the report names the property, the value it received and the constraint that rejected it. That is the behaviour you want in a rolling deployment: the new instance never becomes ready, the old instances keep serving, and the deployment stalls visibly instead of a misconfigured instance accepting traffic and failing requests.

Two practical conditions attach to this. Bean validation needs an implementation on the classpath, which the validation starter provides; without one the constraint annotations are inert and you get no checking at all, silently. And the constraints only cover what you can express declaratively, so a rule spanning two properties — a maximum that must exceed a minimum — belongs in a compact @PostConstruct check or a custom class-level constraint on the properties type, in the same place, so that it fails at the same moment.

The binder also reports failures usefully in its own right. A value that will not convert produces an error naming the property and its origin, meaning the file and line or the environment variable it came from, which is what you need when four sources could have supplied it.

What @Value is still the right tool for

@Value evaluates SpEL, and @ConfigurationProperties does not. So a single value derived from an expression, a value needed inside a @Bean factory method, or a one-off in a small component are all reasonable uses, and rewriting them as a properties class is over-engineering. The line worth drawing is by ownership: anything an operator configures per environment, anything with more than about two related settings, and anything whose wrongness should stop the service, belongs in a validated bound type. Everything else can stay a placeholder.

Bound properties get relaxed name matching, structured values and validation at refresh time; a placeholder gets an exact-name lookup and no checking. The setting that works from YAML on a laptop and vanishes as an environment variable in production is the case that decides it.

Likely follow-ups

  • What does a binding failure report, and what does the origin in that report tell you?
  • How would you bind a duration or a data size without writing a converter?
  • Where do profile-specific documents sit in the precedence order relative to environment variables?
  • How do you expose a bound properties object over Actuator without leaking a credential?

Related questions

Further reading

spring-bootconfiguration-propertiesvalidationexternalised-configfail-fast