Custom Configuration in Spring Applications: A Comprehensive Guide
Configuration management is a critical aspect of any Spring application. As applications grow in complexity, the need for organized, flexible, and environment-specific configuration becomes increasingly important. Spring Framework provides robust mechanisms for handling configuration properties, allowing developers to externalize configuration and make applications more maintainable.
This document explores how to implement custom configurations in Spring applications using both application.properties files and custom configuration files. We'll dive deep into various approaches, best practices, and provide practical examples to illustrate these concepts.
Understanding Spring Configuration Basics
Before we delve into custom configurations, let's establish a foundation by understanding how Spring handles configuration properties by default.
Spring Boot applications typically use application.properties (or application.yml) files to configure various aspects of the application. These files are automatically detected when placed in standard locations:
Inside the application's classpath
In a /config subdirectory in the current directory
In the /config directory directly under the application's root
When the application starts, Spring Boot loads these properties and makes them available throughout the application.
Custom Configuration in application.properties
Basic Property Configuration
The simplest form of custom configuration involves adding your own properties to the application.properties file. Let's look at an example:
# Standard Spring properties
spring.application.name=my-application
server.port=8080
# Custom properties
app.feature.enabled=true
app.cache.timeout=3600
app.service.url=https://api.example.com
app.max-connections=100
Accessing Properties in Spring Components
To access these properties in your Spring components, you can use the @Value annotation:
@RestController
@RequestMapping("/api")
public class ApiController {
@Value("${app.feature.enabled}")
private boolean featureEnabled;
@Value("${app.service.url}")
private String serviceUrl;
@GetMapping("/status")
public String getStatus() {
if (featureEnabled) {
return "Feature is enabled. Service URL: " + serviceUrl;
}
return "Feature is disabled";
}
}
Using ConfigurationProperties
For more structured configuration, Spring Boot provides the @ConfigurationProperties annotation, which allows binding configuration properties to Java objects:
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private boolean featureEnabled;
private int cacheTimeout;
private String serviceUrl;
private int maxConnections;
// Getters and setters
public boolean isFeatureEnabled() {
return featureEnabled;
}
public void setFeatureEnabled(boolean featureEnabled) {
this.featureEnabled = featureEnabled;
}
public int getCacheTimeout() {
return cacheTimeout;
}
public void setCacheTimeout(int cacheTimeout) {
this.cacheTimeout = cacheTimeout;
}
public String getServiceUrl() {
return serviceUrl;
}
public void setServiceUrl(String serviceUrl) {
this.serviceUrl = serviceUrl;
}
public int getMaxConnections() {
return maxConnections;
}
public void setMaxConnections(int maxConnections) {
this.maxConnections = maxConnections;
}
}
Then, you can inject and use this configuration class in your components:
@Service
public class DataService {
private final AppProperties appProperties;
public DataService(AppProperties appProperties) {
this.appProperties = appProperties;
}
public void processData() {
if (appProperties.isFeatureEnabled()) {
// Use the configuration
System.out.println("Processing with timeout: " + appProperties.getCacheTimeout());
System.out.println("Connecting to: " + appProperties.getServiceUrl());
System.out.println("Max connections: " + appProperties.getMaxConnections());
}
}
}
You can also define nested properties in your application.properties:
app.database.url=jdbc:mysql://localhost:3306/mydb
app.database.username=admin
app.database.password=secret
app.database.pool.max-size=20
app.database.pool.min-idle=5
And bind them to nested classes:
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private Database database = new Database();
// Getter and setter for database
public Database getDatabase() {
return database;
}
public void setDatabase(Database database) {
this.database = database;
}
public static class Database {
private String url;
private String username;
private String password;
private Pool pool = new Pool();
// 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 Pool getPool() {
return pool;
}
public void setPool(Pool pool) {
this.pool = pool;
}
public static class Pool {
private int maxSize;
private int minIdle;
// Getters and setters
public int getMaxSize() {
return maxSize;
}
public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
}
public int getMinIdle() {
return minIdle;
}
public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
}
}
}
}
Custom Configuration Files
While application.properties is convenient, sometimes you may want to separate configurations into multiple files for better organization, especially in large applications.
Creating Custom Property Files
Let's create a custom property file named mail-config.properties in the src/main/resources directory:
mail.smtp.host=smtp.example.com
mail.smtp.port=587
mail.username=notification@example.com
mail.password=mailpassword
mail.from=no-reply@example.com
mail.enable-starttls=true
Loading Custom Property Files
To load this custom property file, you need to create a configuration class:
@Configuration
@PropertySource("classpath:mail-config.properties")
public class MailConfiguration {
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
@ConfigurationProperties(prefix = "mail")
public MailProperties mailProperties() {
return new MailProperties();
}
}
And create a corresponding properties class:
public class MailProperties {
private String smtpHost;
private int smtpPort;
private String username;
private String password;
private String from;
private boolean enableStarttls;
// Getters and setters
public String getSmtpHost() {
return smtpHost;
}
public void setSmtpHost(String smtpHost) {
this.smtpHost = smtpHost;
}
public int getSmtpPort() {
return smtpPort;
}
public void setSmtpPort(int smtpPort) {
this.smtpPort = smtpPort;
}
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 getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public boolean isEnableStarttls() {
return enableStarttls;
}
public void setEnableStarttls(boolean enableStarttls) {
this.enableStarttls = enableStarttls;
}
}
Using Environment-Specific Configuration Files
Spring Boot allows you to have environment-specific configuration files. For example:
application-dev.properties
application-test.properties
application-prod.properties
You can activate a specific profile using the spring.profiles.active property:
# In application.properties
spring.profiles.active=dev
Similarly, you can create environment-specific custom property files:
mail-config-dev.properties
mail-config-prod.properties
And load them conditionally:
@Configuration
@PropertySource("classpath:mail-config-${spring.profiles.active:default}.properties")
public class MailConfiguration {
// Configuration code
}
Advanced Configuration Techniques
Sometimes, you might need to load properties from locations determined at runtime. Here's how you can achieve this:
@Configuration
public class DynamicPropertyConfiguration {
@Autowired
private Environment environment;
@Bean
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
String configPath = environment.getProperty("config.path", "/default/path");
Resource resource = new FileSystemResource(configPath + "/custom-config.properties");
configurer.setLocation(resource);
configurer.setIgnoreResourceNotFound(true);
return configurer;
}
}
Encrypting Sensitive Properties
For sensitive information, you might want to encrypt your properties. Spring Cloud Config provides support for encrypted properties:
app.secret.key={cipher}AQA6+Ic4V9M3qrjJ4HbDgJ6BrNrAY7...
To use this feature, you need to include Spring Cloud Config dependencies and configure an encryption key.
Refreshing Properties at Runtime
Spring Cloud provides the @RefreshScope annotation, which allows beans to be refreshed when configuration changes:
@RestController
@RefreshScope
public class ApiController {
@Value("${app.feature.enabled}")
private boolean featureEnabled;
// Controller methods
}
When you trigger a refresh event (e.g., by calling a refresh endpoint), beans with @RefreshScope will be recreated with the new property values.
Architectural Diagram: Configuration Flow in Spring Applications
┌─────────────────────────────────────────────────────────────────┐
│ Configuration Sources │
├───────────────┬───────────────┬────────────────┬────────────────┤
│ application. │ Custom .props │ Environment │ Command Line │
│ properties │ Files │ Variables │ Arguments │
└───────┬───────┴───────┬───────┴────────┬───────┴────────┬───────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Spring Environment Abstraction │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │
│ │ PropertySource │ │ PropertySource │ │PropertySource│ │
│ │ Abstraction │◄───┤ Placeholder │◄───┤ Resolvers │ │
│ └─────────────────┘ └─────────────────┘ └──────────────┘ │
│ │
└───────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Application Components │
├───────────────┬───────────────┬────────────────┬────────────────┤
│ @Value │ Environment │ @Configuration │ @ConfigProperty│
│ Annotation │ Injection │ Properties │ Classes │
└───────────────┴───────────────┴────────────────┴────────────────┘
Example: Complete Configuration System
Let's put everything together with a comprehensive example of a custom configuration system for a hypothetical e-commerce application.
src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── ecommerce/
│ │ ├── EcommerceApplication.java
│ │ ├── config/
│ │ │ ├── AppConfig.java
│ │ │ ├── DatabaseConfig.java
│ │ │ ├── PaymentConfig.java
│ │ │ └── properties/
│ │ │ ├── AppProperties.java
│ │ │ ├── DatabaseProperties.java
│ │ │ └── PaymentProperties.java
│ │ ├── service/
│ │ │ ├── ProductService.java
│ │ │ ├── OrderService.java
│ │ │ └── PaymentService.java
│ │ └── controller/
│ │ └── OrderController.java
│ └── resources/
│ ├── application.properties
│ ├── application-dev.properties
│ ├── application-prod.properties
│ ├── database-config.properties
│ └── payment-config.properties
spring.application.name=ecommerce-service
server.port=8080
spring.profiles.active=dev
# Common application settings
app.name=E-Commerce Platform
app.version=1.0.0
app.support-email=support@example.com
app.feature.recommendations-enabled=true
app.feature.live-chat-enabled=false
app.session-timeout=30
application-dev.properties:
# Development-specific settings
logging.level.root=DEBUG
app.feature.live-chat-enabled=true
application-prod.properties:
# Production-specific settings
logging.level.root=INFO
server.tomcat.max-threads=200
app.session-timeout=15
database-config.properties:
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/ecommerce
db.username=dbuser
db.password=dbpass
db.pool.initial-size=5
db.pool.max-size=20
db.pool.connection-timeout=30000
payment-config.properties:
payment.gateway.url=https://payment.example.com/api
payment.gateway.api-key=test-api-key
payment.gateway.timeout=5000
payment.methods.credit-card=true
payment.methods.paypal=true
payment.methods.crypto=false
@Configuration
@EnableConfigurationProperties(AppProperties.class)
public class AppConfig {
@Autowired
private AppProperties appProperties;
@Bean
public SupportService supportService() {
return new SupportService(appProperties.getSupportEmail());
}
@Bean
public SessionManager sessionManager() {
return new SessionManager(appProperties.getSessionTimeout());
}
}
@Configuration
@PropertySource("classpath:database-config.properties")
@EnableConfigurationProperties(DatabaseProperties.class)
public class DatabaseConfig {
@Autowired
private DatabaseProperties dbProperties;
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setDriverClassName(dbProperties.getDriver());
config.setJdbcUrl(dbProperties.getUrl());
config.setUsername(dbProperties.getUsername());
config.setPassword(dbProperties.getPassword());
config.setMinimumIdle(dbProperties.getPool().getInitialSize());
config.setMaximumPoolSize(dbProperties.getPool().getMaxSize());
config.setConnectionTimeout(dbProperties.getPool().getConnectionTimeout());
return new HikariDataSource(config);
}
}
@Configuration
@PropertySource("classpath:payment-config.properties")
@EnableConfigurationProperties(PaymentProperties.class)
public class PaymentConfig {
@Autowired
private PaymentProperties paymentProperties;
@Bean
public PaymentGatewayClient paymentGatewayClient() {
return new PaymentGatewayClient(
paymentProperties.getGateway().getUrl(),
paymentProperties.getGateway().getApiKey(),
paymentProperties.getGateway().getTimeout()
);
}
@Bean
public PaymentMethodsService paymentMethodsService() {
return new PaymentMethodsService(paymentProperties.getMethods());
}
}
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private String name;
private String version;
private String supportEmail;
private int sessionTimeout;
private FeatureFlags feature = new FeatureFlags();
// Getters and setters
public static class FeatureFlags {
private boolean recommendationsEnabled;
private boolean liveChatEnabled;
// Getters and setters
}
}
@ConfigurationProperties(prefix = "db")
public class DatabaseProperties {
private String driver;
private String url;
private String username;
private String password;
private Pool pool = new Pool();
// Getters and setters
public static class Pool {
private int initialSize;
private int maxSize;
private int connectionTimeout;
// Getters and setters
}
}
@ConfigurationProperties(prefix = "payment")
public class PaymentProperties {
private Gateway gateway = new Gateway();
private Methods methods = new Methods();
// Getters and setters
public static class Gateway {
private String url;
private String apiKey;
private int timeout;
// Getters and setters
}
public static class Methods {
private boolean creditCard;
private boolean paypal;
private boolean crypto;
// Getters and setters
}
}
Using the Configuration in Services
@Service
public class PaymentService {
private final PaymentGatewayClient paymentGatewayClient;
private final PaymentMethodsService paymentMethodsService;
public PaymentService(
PaymentGatewayClient paymentGatewayClient,
PaymentMethodsService paymentMethodsService) {
this.paymentGatewayClient = paymentGatewayClient;
this.paymentMethodsService = paymentMethodsService;
}
public List<String> getAvailablePaymentMethods() {
return paymentMethodsService.getEnabledMethods();
}
public PaymentResult processPayment(Order order, PaymentDetails details) {
// Use the payment gateway client to process payment
return paymentGatewayClient.processPayment(order.getTotalAmount(), details);
}
}
@Service
public class ProductService {
private final DataSource dataSource;
private final AppProperties appProperties;
public ProductService(DataSource dataSource, AppProperties appProperties) {
this.dataSource = dataSource;
this.appProperties = appProperties;
}
public List<Product> getRecommendedProducts(long userId) {
if (appProperties.getFeature().isRecommendationsEnabled()) {
// Fetch personalized recommendations from database
return fetchRecommendationsFromDb(userId);
} else {
// Return generic popular products
return fetchPopularProducts();
}
}
private List<Product> fetchRecommendationsFromDb(long userId) {
// Implementation using dataSource
return new ArrayList<>();
}
private List<Product> fetchPopularProducts() {
// Implementation using dataSource
return new ArrayList<>();
}
}
Architectural Diagram: Custom Configuration System
┌─────────────────────────────────────────────────────────────────┐
│ Configuration Files │
├───────────────┬───────────────┬────────────────┬────────────────┤
│ application. │ application- │ database- │ payment- │
│ properties │ {env}.props │ config.props │ config.props │
└───────┬───────┴───────┬───────┴────────┬───────┴────────┬───────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Configuration Classes │
├───────────────┬───────────────┬────────────────┬────────────────┤
│ AppConfig │ DatabaseConfig│ PaymentConfig │ Other Config │
│ @Configuration│ @PropertySrc │ @PropertySrc │ Classes │
└───────┬───────┴───────┬───────┴────────┬───────┴────────┬───────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Property Classes │
├───────────────┬───────────────┬────────────────┬────────────────┤
│ AppProperties │ DatabaseProps │ PaymentProps │ Other Property │
│ @ConfigProps │ @ConfigProps │ @ConfigProps │ Classes │
└───────┬───────┴───────┬───────┴────────┬───────┴────────┬───────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Application Services │
├───────────────┬───────────────┬────────────────┬────────────────┤
│ ProductService│ OrderService │ PaymentService │ Other Services │
└───────┬───────┴───────┬───────┴────────┬───────┴────────┬───────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Controllers/API Layer │
└─────────────────────────────────────────────────────────────────┘
Best Practices for Custom Configuration
1. Organize Properties Logically
Group related properties together with meaningful prefixes. For example, all database-related properties should start with db. or database..
2. Validate Configuration Properties
Use validation annotations to ensure that your configuration properties meet your requirements:
@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {
@NotBlank
private String name;
@Email
private String supportEmail;
@Min(5)
@Max(60)
private int sessionTimeout;
// Getters and setters
}
3. Document Your Properties
Use the @ConfigurationProperties metadata to document your properties:
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private String name;
private String supportEmail;
private int sessionTimeout;
// Getters and setters
}
Spring Boot's relaxed binding allows properties to be specified in different formats:
Kebab case: app.support-email
Camel case: app.supportEmail
Underscore notation: app.support_email
Environment variables: APP_SUPPORT_EMAIL
5. Provide Default Values
Always provide sensible default values for your properties:
@Value("${app.feature.enabled:false}")
private boolean featureEnabled;
Or in configuration classes:
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private String name = "Default App";
private int timeout = 30;
// Getters and setters
}
6. Use Profiles for Environment-Specific Configuration
Leverage Spring profiles to manage environment-specific configurations:
# application-dev.properties
app.feature.debug=true
# application-prod.properties
app.feature.debug=false
7. Secure Sensitive Properties
For sensitive information like passwords and API keys:
Use environment variables
Use a secure vault like HashiCorp Vault or AWS Secrets Manager
8. Centralize Configuration for Microservices
For microservice architectures, consider using Spring Cloud Config Server to centralize configuration management.
Custom configuration in Spring applications provides flexibility and organization for your application settings. By leveraging Spring's powerful configuration mechanisms, you can create a maintainable, environment-aware configuration system that adapts to your application's needs.
This document has covered:
Basic property configuration in application.properties
Creating and using custom configuration files
Binding properties to Java objects with @ConfigurationProperties
Environment-specific configuration
Advanced techniques like dynamic property loading and property encryption
A comprehensive example of a complete configuration system
Best practices for managing configuration in Spring applications
By following these patterns and practices, you can build a robust configuration system that grows with your application and supports your development workflow across different environments.