The @ConfigurationProperties annotation in Spring Boot provides a powerful way to externalize configuration and bind it to strongly-typed Java objects. This article explores various aspects of using configuration properties in Spring Boot applications.
Basic @ConfigurationProperties Usage
1. Simple Configuration Properties
@Configuration @ConfigurationProperties(prefix = "app") public class AppProperties { private String name; private String version; private String description; // Default values private boolean enabled = true; private int timeout = 30; // Getters and setters public String getName() { return name; } public void setName(String name) { this.name = name; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } } application.yml:
app: name: "My Application" version: "1.0.0" description: "A Spring Boot application" enabled: true timeout: 60
2. Using @ConfigurationProperties with @Bean
@Configuration public class AppConfig { @Bean @ConfigurationProperties(prefix = "database") public DatabaseProperties databaseProperties() { return new DatabaseProperties(); } } public class DatabaseProperties { private String url; private String username; private String password; private String driverClassName; private PoolProperties pool; // Nested static class public static class PoolProperties { private int initialSize = 5; private int maxActive = 20; private int maxWait = 60000; private boolean testOnBorrow = true; // Getters and setters public int getInitialSize() { return initialSize; } public void setInitialSize(int initialSize) { this.initialSize = initialSize; } public int getMaxActive() { return maxActive; } public void setMaxActive(int maxActive) { this.maxActive = maxActive; } public int getMaxWait() { return maxWait; } public void setMaxWait(int maxWait) { this.maxWait = maxWait; } public boolean isTestOnBorrow() { return testOnBorrow; } public void setTestOnBorrow(boolean testOnBorrow) { this.testOnBorrow = testOnBorrow; } } // Getters and setters public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public PoolProperties getPool() { return pool; } public void setPool(PoolProperties pool) { this.pool = pool; } } application.yml:
database: url: "jdbc:mysql://localhost:3306/mydb" username: "admin" password: "secret" driver-class-name: "com.mysql.cj.jdbc.Driver" pool: initial-size: 10 max-active: 50 max-wait: 30000 test-on-borrow: true
Advanced Configuration Properties
3. Lists and Maps
@Configuration @ConfigurationProperties(prefix = "application") public class ApplicationProperties { private List<String> servers = new ArrayList<>(); private Map<String, String> metadata = new HashMap<>(); private List<Endpoint> endpoints = new ArrayList<>(); private Map<String, Credentials> users = new HashMap<>(); // Nested classes public static class Endpoint { private String name; private String url; private int timeout; private boolean secure; // Getters and setters public String getName() { return name; } public void setName(String name) { this.name = name; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } public boolean isSecure() { return secure; } public void setSecure(boolean secure) { this.secure = secure; } } public static class Credentials { private String username; private String password; private List<String> roles = new ArrayList<>(); // Getters and setters public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public List<String> getRoles() { return roles; } public void setRoles(List<String> roles) { this.roles = roles; } } // Getters and setters public List<String> getServers() { return servers; } public void setServers(List<String> servers) { this.servers = servers; } public Map<String, String> getMetadata() { return metadata; } public void setMetadata(Map<String, String> metadata) { this.metadata = metadata; } public List<Endpoint> getEndpoints() { return endpoints; } public void setEndpoints(List<Endpoint> endpoints) { this.endpoints = endpoints; } public Map<String, Credentials> getUsers() { return users; } public void setUsers(Map<String, Credentials> users) { this.users = users; } } application.yml:
application: servers: - "server1.example.com" - "server2.example.com" - "server3.example.com" metadata: environment: "production" region: "us-east-1" version: "2.1.0" endpoints: - name: "api" url: "https://api.example.com/v1" timeout: 5000 secure: true - name: "health" url: "https://api.example.com/health" timeout: 1000 secure: false users: admin: username: "admin" password: "admin123" roles: - "ROLE_ADMIN" - "ROLE_USER" user: username: "user" password: "user123" roles: - "ROLE_USER"
4. Duration and Data Size
@Configuration @ConfigurationProperties(prefix = "server") public class ServerProperties { private Duration timeout; private Duration readTimeout; private DataSize maxFileSize; private DataSize maxRequestSize; private Duration cacheTtl; // Getters and setters public Duration getTimeout() { return timeout; } public void setTimeout(Duration timeout) { this.timeout = timeout; } public Duration getReadTimeout() { return readTimeout; } public void setReadTimeout(Duration readTimeout) { this.readTimeout = readTimeout; } public DataSize getMaxFileSize() { return maxFileSize; } public void setMaxFileSize(DataSize maxFileSize) { this.maxFileSize = maxFileSize; } public DataSize getMaxRequestSize() { return maxRequestSize; } public void setMaxRequestSize(DataSize maxRequestSize) { this.maxRequestSize = maxRequestSize; } public Duration getCacheTtl() { return cacheTtl; } public void setCacheTtl(Duration cacheTtl) { this.cacheTtl = cacheTtl; } } application.yml:
server: timeout: 30s read-timeout: 1m max-file-size: 10MB max-request-size: 20MB cache-ttl: 1h
Validation with @ConfigurationProperties
5. Using JSR-303 Validation
@Configuration @ConfigurationProperties(prefix = "validation") @Validated public class ValidationProperties { @NotNull @NotEmpty private String apiKey; @Email @NotEmpty private String adminEmail; @Min(1) @Max(100) private int maxConnections; @Pattern(regexp = "^https?://.*") private String baseUrl; @Valid private SecurityProperties security; public static class SecurityProperties { @NotNull private Boolean enabled; @Size(min = 8, max = 100) private String secretKey; @AssertTrue(message = "SSL must be enabled in production") public boolean isSslEnabled() { return !"production".equals(System.getenv("SPRING_PROFILES_ACTIVE")) || enabled; } // Getters and setters public Boolean getEnabled() { return enabled; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } public String getSecretKey() { return secretKey; } public void setSecretKey(String secretKey) { this.secretKey = secretKey; } } // Getters and setters public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } public String getAdminEmail() { return adminEmail; } public void setAdminEmail(String adminEmail) { this.adminEmail = adminEmail; } public int getMaxConnections() { return maxConnections; } public void setMaxConnections(int maxConnections) { this.maxConnections = maxConnections; } public String getBaseUrl() { return baseUrl; } public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } public SecurityProperties getSecurity() { return security; } public void setSecurity(SecurityProperties security) { this.security = security; } } application.yml:
validation: api-key: "abc123" admin-email: "[email protected]" max-connections: 50 base-url: "https://api.example.com" security: enabled: true secret-key: "my-secret-key-123"
Profile-specific Configuration
6. Multiple Environments
@Configuration @ConfigurationProperties(prefix = "environment") public class EnvironmentProperties { private String name; private DatabaseConfig database; private CacheConfig cache; private LoggingConfig logging; public static class DatabaseConfig { private String url; private boolean showSql = false; private String ddlAuto = "validate"; // Getters and setters public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public boolean isShowSql() { return showSql; } public void setShowSql(boolean showSql) { this.showSql = showSql; } public String getDdlAuto() { return ddlAuto; } public void setDdlAuto(String ddlAuto) { this.ddlAuto = ddlAuto; } } public static class CacheConfig { private Duration ttl = Duration.ofMinutes(30); private long maxSize = 1000; private boolean enabled = true; // Getters and setters public Duration getTtl() { return ttl; } public void setTtl(Duration ttl) { this.ttl = ttl; } public long getMaxSize() { return maxSize; } public void setMaxSize(long maxSize) { this.maxSize = maxSize; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } } public static class LoggingConfig { private Level level = Level.INFO; private String filePath; private int maxHistory = 30; // Getters and setters public Level getLevel() { return level; } public void setLevel(Level level) { this.level = level; } public String getFilePath() { return filePath; } public void setFilePath(String filePath) { this.filePath = filePath; } public int getMaxHistory() { return maxHistory; } public void setMaxHistory(int maxHistory) { this.maxHistory = maxHistory; } } // Getters and setters public String getName() { return name; } public void setName(String name) { this.name = name; } public DatabaseConfig getDatabase() { return database; } public void setDatabase(DatabaseConfig database) { this.database = database; } public CacheConfig getCache() { return cache; } public void setCache(CacheConfig cache) { this.cache = cache; } public LoggingConfig getLogging() { return logging; } public void setLogging(LoggingConfig logging) { this.logging = logging; } } application-dev.yml:
environment: name: "development" database: url: "jdbc:h2:mem:testdb" show-sql: true ddl-auto: "create-drop" cache: ttl: 5m max-size: 100 enabled: false logging: level: DEBUG file-path: "./logs/dev.log"
application-prod.yml:
environment: name: "production" database: url: "jdbc:mysql://prod-db:3306/app" show-sql: false ddl-auto: "validate" cache: ttl: 1h max-size: 10000 enabled: true logging: level: WARN file-path: "/var/log/app/app.log" max-history: 60
Using @ConfigurationProperties in Services
7. Injecting Configuration Properties
@Service public class ApiService { private final ApplicationProperties appProperties; private final DatabaseProperties dbProperties; @Autowired public ApiService(ApplicationProperties appProperties, DatabaseProperties dbProperties) { this.appProperties = appProperties; this.dbProperties = dbProperties; } public void printConfiguration() { System.out.println("App Name: " + appProperties.getName()); System.out.println("Database URL: " + dbProperties.getUrl()); appProperties.getEndpoints().forEach(endpoint -> { System.out.println("Endpoint: " + endpoint.getName() + " -> " + endpoint.getUrl()); }); } @PostConstruct public void initialize() { if (appProperties.isEnabled()) { System.out.println("Application is enabled with version: " + appProperties.getVersion()); } } } @RestController public class ConfigController { @Autowired private EnvironmentProperties envProperties; @GetMapping("/config") public Map<String, Object> getConfig() { Map<String, Object> config = new HashMap<>(); config.put("environment", envProperties.getName()); config.put("database", envProperties.getDatabase()); config.put("cache", envProperties.getCache()); return config; } } Constructor Binding (Spring Boot 2.2+)
8. Immutable Configuration Properties
@ConfigurationProperties(prefix = "immutable") @ConstructorBinding public class ImmutableProperties { private final String name; private final int port; private final List<String> hosts; private final DatabaseConfig database; @ConstructorBinding public ImmutableProperties(String name, int port, List<String> hosts, DatabaseConfig database) { this.name = name; this.port = port; this.hosts = hosts != null ? List.copyOf(hosts) : List.of(); this.database = database; } public static class DatabaseConfig { private final String url; private final String username; private final String password; @ConstructorBinding public DatabaseConfig(String url, String username, String password) { this.url = url; this.username = username; this.password = password; } // Getters only (immutable) public String getUrl() { return url; } public String getUsername() { return username; } public String getPassword() { return password; } } // Getters only (immutable) public String getName() { return name; } public int getPort() { return port; } public List<String> getHosts() { return hosts; } public DatabaseConfig getDatabase() { return database; } } Enable in your main class:
@SpringBootApplication @EnableConfigurationProperties(ImmutableProperties.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } Testing Configuration Properties
9. Unit Tests for Configuration Properties
@SpringBootTest @EnableConfigurationProperties({ AppProperties.class, DatabaseProperties.class, ValidationProperties.class }) class ConfigurationPropertiesTest { @Autowired private AppProperties appProperties; @Autowired private DatabaseProperties databaseProperties; @Autowired private ValidationProperties validationProperties; @Test void testAppPropertiesBinding() { assertThat(appProperties.getName()).isEqualTo("My Application"); assertThat(appProperties.getVersion()).isEqualTo("1.0.0"); assertThat(appProperties.isEnabled()).isTrue(); } @Test void testDatabasePropertiesBinding() { assertThat(databaseProperties.getUrl()).isEqualTo("jdbc:mysql://localhost:3306/mydb"); assertThat(databaseProperties.getPool().getInitialSize()).isEqualTo(10); assertThat(databaseProperties.getPool().getMaxActive()).isEqualTo(50); } @Test void testValidationProperties() { assertThat(validationProperties.getApiKey()).isEqualTo("abc123"); assertThat(validationProperties.getAdminEmail()).isEqualTo("[email protected]"); assertThat(validationProperties.getSecurity().isEnabled()).isTrue(); } @TestConfiguration @TestPropertySource(properties = { "app.name=Test Application", "app.version=2.0.0", "app.enabled=false", "database.url=jdbc:h2:mem:testdb", "database.pool.initial-size=5", "validation.api-key=test-key", "[email protected]", "validation.security.enabled=true" }) static class TestConfig { } } // Test with @TestPropertySource @DataJpaTest @TestPropertySource(properties = { "database.url=jdbc:h2:mem:testdb", "database.username=test", "database.password=test" }) class DatabaseTest { // Test database configuration } Best Practices
10. Configuration Tips and Patterns
@Configuration @ConfigurationProperties(prefix = "best-practices") public class BestPracticesProperties { // Use meaningful default values private int connectionTimeout = 5000; private int readTimeout = 10000; private int maxRetries = 3; // Use appropriate data types private Duration cacheDuration = Duration.ofMinutes(30); private DataSize maxUploadSize = DataSize.ofMegabytes(10); // Group related properties private RedisConfig redis = new RedisConfig(); private EmailConfig email = new EmailConfig(); public static class RedisConfig { private String host = "localhost"; private int port = 6379; private Duration timeout = Duration.ofSeconds(5); private String password; // Getters and setters public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public Duration getTimeout() { return timeout; } public void setTimeout(Duration timeout) { this.timeout = timeout; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } public static class EmailConfig { private String host; private int port = 587; private String username; private String password; private boolean auth = true; private boolean starttls = true; // Getters and setters public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public boolean isAuth() { return auth; } public void setAuth(boolean auth) { this.auth = auth; } public boolean isStarttls() { return starttls; } public void setStarttls(boolean starttls) { this.starttls = starttls; } } // Getters and setters public int getConnectionTimeout() { return connectionTimeout; } public void setConnectionTimeout(int connectionTimeout) { this.connectionTimeout = connectionTimeout; } public int getReadTimeout() { return readTimeout; } public void setReadTimeout(int readTimeout) { this.readTimeout = readTimeout; } public int getMaxRetries() { return maxRetries; } public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } public Duration getCacheDuration() { return cacheDuration; } public void setCacheDuration(Duration cacheDuration) { this.cacheDuration = cacheDuration; } public DataSize getMaxUploadSize() { return maxUploadSize; } public void setMaxUploadSize(DataSize maxUploadSize) { this.maxUploadSize = maxUploadSize; } public RedisConfig getRedis() { return redis; } public void setRedis(RedisConfig redis) { this.redis = redis; } public EmailConfig getEmail() { return email; } public void setEmail(EmailConfig email) { this.email = email; } } Conclusion
The @ConfigurationProperties annotation provides a powerful way to manage external configuration in Spring Boot applications. Key benefits include:
- Type-safe configuration with compile-time checking
- IDE support for auto-completion and validation
- Validation using JSR-303 annotations
- Nested properties for complex configuration structures
- Profile-specific configuration support
- Immutable configuration with constructor binding
By following these patterns and best practices, you can create maintainable, testable, and flexible configuration management in your Spring Boot applications.