Introduction
When using @ConfigurationProperties in Spring Boot, default values are set by assigning initial values to the fields of your configuration class. If a property is not defined in application.properties or application.yml, the field retains its Java-assigned default. This is simpler and more reliable than using @Value with default syntax (${prop:default}), because the defaults live directly in the configuration class alongside their field types and validation annotations.
Basic Default Values
1import org.springframework.boot.context.properties.ConfigurationProperties;
2
3@ConfigurationProperties(prefix = "app")
4public class AppProperties {
5
6 private String name = "MyApp"; // Default: "MyApp"
7 private int maxRetries = 3; // Default: 3
8 private long timeoutMs = 5000; // Default: 5000
9 private boolean debugMode = false; // Default: false
10 private String apiUrl = "https://api.example.com";
11
12 // Getters and setters required for binding
13 public String getName() { return name; }
14 public void setName(String name) { this.name = name; }
15
16 public int getMaxRetries() { return maxRetries; }
17 public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; }
18
19 public long getTimeoutMs() { return timeoutMs; }
20 public void setTimeoutMs(long timeoutMs) { this.timeoutMs = timeoutMs; }
21
22 public boolean isDebugMode() { return debugMode; }
23 public void setDebugMode(boolean debugMode) { this.debugMode = debugMode; }
24
25 public String getApiUrl() { return apiUrl; }
26 public void setApiUrl(String apiUrl) { this.apiUrl = apiUrl; }
27}
1# application.yml — only override what you need
2app:
3 name: ProductionApp
4 max-retries: 5
5 # timeout-ms not specified — uses default 5000
6 # debug-mode not specified — uses default false
7 # api-url not specified — uses default https://api.example.com
Enabling the Configuration Class
1import org.springframework.boot.context.properties.EnableConfigurationProperties;
2import org.springframework.context.annotation.Configuration;
3
4@Configuration
5@EnableConfigurationProperties(AppProperties.class)
6public class AppConfig {
7}
8
9// Or use @ConfigurationPropertiesScan on the main class (Spring Boot 2.2+)
10@SpringBootApplication
11@ConfigurationPropertiesScan
12public class Application {
13 public static void main(String[] args) {
14 SpringApplication.run(Application.class, args);
15 }
16}
Collection Defaults
1@ConfigurationProperties(prefix = "app")
2public class AppProperties {
3
4 private List<String> allowedOrigins = List.of("http://localhost:3000");
5 private Map<String, String> headers = Map.of(
6 "X-App-Name", "MyApp",
7 "X-App-Version", "1.0"
8 );
9 private Set<String> enabledFeatures = new HashSet<>(Set.of("auth", "logging"));
10
11 // Getters and setters
12 public List<String> getAllowedOrigins() { return allowedOrigins; }
13 public void setAllowedOrigins(List<String> allowedOrigins) {
14 this.allowedOrigins = allowedOrigins;
15 }
16
17 public Map<String, String> getHeaders() { return headers; }
18 public void setHeaders(Map<String, String> headers) {
19 this.headers = headers;
20 }
21
22 public Set<String> getEnabledFeatures() { return enabledFeatures; }
23 public void setEnabledFeatures(Set<String> enabledFeatures) {
24 this.enabledFeatures = enabledFeatures;
25 }
26}
1# Overriding replaces the entire collection, not merges
2app:
3 allowed-origins:
4 - https://example.com
5 - https://app.example.com
6 # headers not specified — uses default map
Nested Object Defaults
1@ConfigurationProperties(prefix = "app")
2public class AppProperties {
3
4 private final Database database = new Database();
5 private final Cache cache = new Cache();
6
7 public Database getDatabase() { return database; }
8 public Cache getCache() { return cache; }
9
10 public static class Database {
11 private String url = "jdbc:h2:mem:testdb";
12 private int poolSize = 10;
13 private long connectionTimeoutMs = 30000;
14
15 // Getters and setters
16 public String getUrl() { return url; }
17 public void setUrl(String url) { this.url = url; }
18 public int getPoolSize() { return poolSize; }
19 public void setPoolSize(int poolSize) { this.poolSize = poolSize; }
20 public long getConnectionTimeoutMs() { return connectionTimeoutMs; }
21 public void setConnectionTimeoutMs(long ms) { this.connectionTimeoutMs = ms; }
22 }
23
24 public static class Cache {
25 private boolean enabled = true;
26 private int ttlSeconds = 300;
27
28 public boolean isEnabled() { return enabled; }
29 public void setEnabled(boolean enabled) { this.enabled = enabled; }
30 public int getTtlSeconds() { return ttlSeconds; }
31 public void setTtlSeconds(int ttlSeconds) { this.ttlSeconds = ttlSeconds; }
32 }
33}
1# Override only specific nested properties
2app:
3 database:
4 url: jdbc:postgresql://localhost:5432/mydb
5 pool-size: 20
6 # connection-timeout-ms uses default 30000
7 # cache section not specified — all defaults apply
Constructor Binding (Spring Boot 2.2+)
With @ConstructorBinding, defaults are specified as constructor parameter defaults (Kotlin) or via @DefaultValue:
1import org.springframework.boot.context.properties.ConfigurationProperties;
2import org.springframework.boot.context.properties.bind.DefaultValue;
3
4@ConfigurationProperties(prefix = "app")
5public record AppProperties(
6 @DefaultValue("MyApp") String name,
7 @DefaultValue("3") int maxRetries,
8 @DefaultValue("5000") long timeoutMs,
9 @DefaultValue("false") boolean debugMode
10) {}
1// Kotlin — natural default parameter syntax
2@ConfigurationProperties(prefix = "app")
3data class AppProperties(
4 val name: String = "MyApp",
5 val maxRetries: Int = 3,
6 val timeoutMs: Long = 5000,
7 val debugMode: Boolean = false
8)
Validation with Defaults
1import jakarta.validation.constraints.Min;
2import jakarta.validation.constraints.NotBlank;
3import org.springframework.validation.annotation.Validated;
4
5@Validated
6@ConfigurationProperties(prefix = "app")
7public class AppProperties {
8
9 @NotBlank
10 private String name = "MyApp";
11
12 @Min(1)
13 private int maxRetries = 3;
14
15 @Min(100)
16 private long timeoutMs = 5000;
17
18 // If someone sets app.max-retries=0 in properties,
19 // validation fails with ConstraintViolationException
20}
Add spring-boot-starter-validation dependency for JSR-303 validation:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
Common Pitfalls
Missing setter methods: @ConfigurationProperties uses setter-based binding by default. Without setters, Spring cannot override the defaults from properties files. The field keeps its Java default, and no error is thrown — the property is silently ignored.
Collection defaults replaced, not merged: When you specify app.allowed-origins in YAML, the entire list replaces the default. Spring does not merge the new values with the defaults. If you need additive behavior, handle it in application code.
@DefaultValue only works with constructor binding: The @DefaultValue annotation is for record-style or constructor-based binding. Using it on a field of a mutable class has no effect. For mutable classes, assign the default directly to the field.
Null defaults for wrapper types: private Integer count; defaults to null, not 0. If code accesses count without null checking, it throws NullPointerException. Use primitive types for required numeric properties or initialize wrapper types explicitly.
Property name binding rules: Spring Boot uses relaxed binding: app.maxRetries, app.max-retries, app.max_retries, and APP_MAXRETRIES all bind to maxRetries. However, in application.properties/application.yml, kebab-case (max-retries) is the recommended convention.
Summary
Set defaults by assigning initial values to fields in the @ConfigurationProperties class
Only properties explicitly defined in application.properties/application.yml override the defaults
Use @DefaultValue for constructor-binding or record classes
Nested objects get defaults through field initialization of inner static classes
Collections are fully replaced when overridden, not merged with defaults
Add @Validated with JSR-303 annotations to enforce constraints on both defaults and overrides