Spring Boot
@ConfigurationProperties Annotation
@ConfigurationProperties is Spring Boot's mechanism for binding a prefix of properties to a strongly-typed Java class. It replaces scattered @Value annotations with a single, validated, IDE-autocompleted configuration object — and is the recommended approach for any non-trivial application configuration.
Anatomy of @ConfigurationProperties
@ConfigurationProperties has one primary attribute — prefix — which defines the namespace of properties the annotation binds. Every field in the annotated class is matched against properties under that prefix using Spring Boot's relaxed binding rules.
The annotation itself does nothing at runtime until the class is registered as a Spring bean. There are three ways to register it: @ConfigurationPropertiesScan on the main application class (scans the package automatically), @EnableConfigurationProperties(MyProps.class) on any @Configuration class (explicit registration), or @Component directly on the properties class (simplest, but mixes configuration and component concerns).
Spring Boot instantiates the class, binds all matching properties from the full property source hierarchy — application.yml, environment variables, command-line arguments, and all other sources — and registers the result as a bean. Any other bean can then inject it normally.
Java
// The annotation — prefix is the only required attribute:
@ConfigurationProperties(prefix = "app")
// Full attribute reference:
@ConfigurationProperties(
prefix = "app", // property namespace — required
ignoreUnknownFields = true, // default: true — unknown keys silently ignored
ignoreInvalidFields = false // default: false — type mismatch throws on startup
)
// Three registration approaches:
// 1. @ConfigurationPropertiesScan — scans the package (preferred):
@SpringBootApplication
@ConfigurationPropertiesScan
public class MyApplication { }
// 2. @EnableConfigurationProperties — explicit per-class registration:
@Configuration
@EnableConfigurationProperties(AppProperties.class)
public class AppConfig { }
// 3. @Component on the properties class itself:
@ConfigurationProperties(prefix = "app")
@Component
public class AppProperties { }Binding Mechanics and Relaxed Binding
Spring Boot normalizes both the property key and the field name before matching — this is called relaxed binding. A single field name resolves from multiple source formats, so YAML, environment variables, and command-line arguments all bind to the same field without any additional configuration.
Java
// Field declared as:
private String maxPoolSize;
// All of these bind to the same field:
// application.yml key: app.max-pool-size
// application.yml key: app.maxPoolSize
// Environment variable: APP_MAX_POOL_SIZE
// Command-line argument: --app.max-pool-size=20
// JVM system property: app.max-pool-size=20
// Spring normalizes everything to lowercase + no separators before matching.
// ── Type conversion ───────────────────────────────────────────────────
// Spring Boot converts property strings to field types automatically:
private int port; // "8080" → 8080
private boolean enabled; // "true" → true
private Duration timeout; // "5s" → Duration.ofSeconds(5)
// "500ms" → Duration.ofMillis(500)
// "PT5S" → ISO-8601 also accepted
private DataSize maxUpload; // "10MB" → DataSize.ofMegabytes(10)
// "1GB" → DataSize.ofGigabytes(1)
private List<String> origins; // "a,b,c" → ["a", "b", "c"]
private URI serviceUrl; // string → URI
private InetAddress host; // "localhost" → InetAddress
// application.yml:
app:
port: 8080
enabled: true
timeout: 5s
max-upload: 10MB
origins:
- https://myapp.com
- https://admin.myapp.com
service-url: https://api.myapp.com
host: localhostignoreUnknownFields and ignoreInvalidFields
Two boolean attributes control how @ConfigurationProperties handles mismatches between the YAML and the class. Understanding their defaults prevents silent misconfiguration and startup surprises.
Java
// ── ignoreUnknownFields (default: true) ──────────────────────────────
// Unknown property keys in YAML that have no matching field are silently ignored.
@ConfigurationProperties(prefix = "app", ignoreUnknownFields = false)
public class StrictAppProperties {
private String name;
// No field for "app.typo-key"
}
# application.yml:
app:
name: MyApp
typo-key: oops # with ignoreUnknownFields=false → startup failure:
# "Could not bind properties to StrictAppProperties:
# prefix=app, ignoreUnknownFields=false;
# property typo-key not found in target"
// Use ignoreUnknownFields=false when you want strict config hygiene —
// any unrecognized key in YAML fails fast rather than silently disappearing.
// ── ignoreInvalidFields (default: false) ─────────────────────────────
// Invalid field values (wrong type, unconvertible) throw a startup exception by default.
@ConfigurationProperties(prefix = "app", ignoreInvalidFields = true)
public class LenientAppProperties {
private int port = 8080;
}
# application.yml:
app:
port: not-a-number # with ignoreInvalidFields=false (default) → startup failure
# with ignoreInvalidFields=true → field keeps default (8080)
// ignoreInvalidFields=true is rarely appropriate for production —
// silent fallback to defaults masks misconfiguration.
// Prefer the default (false) and fix the config.@ConfigurationProperties vs @Value
Both inject properties into beans, but they serve different use cases. @Value is appropriate for a single injected scalar; @ConfigurationProperties is the right choice for any group of related properties, nested structures, collections, or validation.
Java
// ── @Value — fine for a single scalar ────────────────────────────────
@Component
public class SimpleBean {
@Value("${server.port:8080}")
private int port;
}
// ── @Value limitations ────────────────────────────────────────────────
// 1. No validation:
@Value("${app.max-retries}")
private int maxRetries; // no @Min/@Max — invalid value discovered at runtime
// 2. No IDE auto-completion for the key string:
@Value("${app.jwt.sercet}") // typo in string — compiles fine, fails at runtime
// 3. No collection binding without SpEL workarounds:
@Value("#{${app.allowed-origins}.split(',')}")
private String[] origins; // fragile
// 4. Scattered across beans — no single place to see all config:
// AuthService.java: @Value("${app.jwt.secret}")
// TokenService.java: @Value("${app.jwt.expiration}")
// IssuerService.java: @Value("${app.jwt.issuer}")
// ── @ConfigurationProperties — the structured alternative ─────────────
@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {
@Valid
private final Jwt jwt = new Jwt();
private List<String> allowedOrigins = new ArrayList<>();
public static class Jwt {
@NotBlank private String secret;
@Positive private long expiration = 86400;
@NotBlank private String issuer;
// getters + setters
}
// getters + setters
}
// ── Decision guide ────────────────────────────────────────────────────
// Use @Value when:
// - Injecting a single scalar with a clear default
// - The property is from a third-party prefix you don't own
// - The bean is a simple @Component with one config dependency
// Use @ConfigurationProperties when:
// - Binding 2+ related properties
// - Any nesting, lists, or maps are involved
// - You need Bean Validation on config values
// - You want IDE auto-completion for your custom keys
// - The config object will be injected into multiple beansConstructor Binding
By default, @ConfigurationProperties uses setter binding — Spring instantiates the class and calls setters for each property. Constructor binding (available from Spring Boot 2.2, the only mode for records) is immutable: Spring calls the constructor directly and never uses setters. Immutable configuration is safer and more explicit about required vs optional values.
Java
// ── Setter binding (default) — mutable, setters required: ───────────
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private String name;
private int port = 8080;
public void setName(String name) { this.name = name; }
public void setPort(int port) { this.port = port; }
public String getName() { return name; }
public int getPort() { return port; }
}
// ── Constructor binding — immutable, no setters needed: ───────────────
@ConfigurationProperties(prefix = "app")
@ConstructorBinding // required before Spring Boot 3.0 for non-record classes
public class AppProperties {
private final String name;
private final int port;
private final JwtProperties jwt;
// Spring calls this constructor — all fields set once, never mutated:
public AppProperties(
String name,
@DefaultValue("8080") int port, // @DefaultValue supplies fallback
JwtProperties jwt
) {
this.name = name;
this.port = port;
this.jwt = jwt;
}
public record JwtProperties(
String secret,
@DefaultValue("86400") long expiration,
String issuer
) { }
// Getters only — no setters:
public String getName() { return name; }
public int getPort() { return port; }
public JwtProperties getJwt() { return jwt; }
}
// Spring Boot 3.0+: @ConstructorBinding is inferred automatically
// when a single constructor is present. Needed only when multiple
// constructors exist and you want to designate one explicitly.
// Records always use constructor binding — no annotation needed:
@ConfigurationProperties(prefix = "app")
public record AppProperties(
@NotBlank String name,
@DefaultValue("8080") int port,
JwtProperties jwt
) { }Registering with @EnableConfigurationProperties
When @ConfigurationPropertiesScan is not used, @EnableConfigurationProperties registers specific classes explicitly. This is useful for library authors and cases where you want to limit which properties classes are auto-detected.
Java
// Explicit registration on a @Configuration class:
@Configuration
@EnableConfigurationProperties({
AppProperties.class,
DatabaseProperties.class,
SecurityProperties.class
})
public class AppConfig { }
// The registered classes do NOT need @Component or @ConfigurationPropertiesScan.
// @EnableConfigurationProperties registers them as beans directly.
// ── Library / starter pattern ──────────────────────────────────────────
// When writing a Spring Boot auto-configuration library,
// use @EnableConfigurationProperties in your auto-config class
// so consumers don't need to scan for your properties class:
@AutoConfiguration
@EnableConfigurationProperties(MyLibraryProperties.class)
@ConditionalOnProperty(prefix = "my-library", name = "enabled", havingValue = "true")
public class MyLibraryAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyLibraryService myLibraryService(MyLibraryProperties props) {
return new MyLibraryService(props.getApiKey(), props.getTimeout());
}
}
@ConfigurationProperties(prefix = "my-library")
public class MyLibraryProperties {
private boolean enabled = false;
private String apiKey;
private Duration timeout = Duration.ofSeconds(5);
// getters + setters
}
# Consumer's application.yml — auto-completed by the processor:
my-library:
enabled: true
api-key: ${MY_LIBRARY_API_KEY}
timeout: 10sDeprecating Properties
When you rename or remove a configuration property, annotate the old field with @DeprecatedConfigurationProperty. The annotation processor includes the deprecation in the generated metadata, so IDEs show a strikethrough and warning on the old key.
Java
@ConfigurationProperties(prefix = "app")
public class AppProperties {
// New field — current name:
private Duration sessionTimeout = Duration.ofMinutes(30);
// Old getter — kept for backwards compatibility, marked deprecated:
@DeprecatedConfigurationProperty(
reason = "Replaced by app.session-timeout (Duration)",
replacement = "app.session-timeout"
)
@Deprecated
public int getSessionTimeoutSeconds() {
return (int) sessionTimeout.getSeconds();
}
// Old setter — still binds the old key during the deprecation window:
@Deprecated
public void setSessionTimeoutSeconds(int seconds) {
this.sessionTimeout = Duration.ofSeconds(seconds);
}
public Duration getSessionTimeout() { return sessionTimeout; }
public void setSessionTimeout(Duration sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
}
# application.yml — old key still works but IDE shows it struck through:
app:
session-timeout-seconds: 1800 # deprecated — IDE warns, still binds
# Migrate to:
app:
session-timeout: 30m # new key — Duration format