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.