Distributed configuration management is essential for modern cloud-native applications to manage configuration across multiple services, environments, and instances consistently. This guide covers various approaches and tools for distributed configuration management in Java applications.
1. Spring Cloud Config
Server Configuration
Config Server Dependencies
<!-- pom.xml --> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2022.0.4</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
Config Server Application
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
application.yml - Config Server
server:
port: 8888
spring:
application:
name: config-server
security:
user:
name: admin
password: ${CONFIG_SERVER_PASSWORD:secret}
cloud:
config:
server:
git:
uri: https://github.com/my-org/config-repo
search-paths: '{application}'
default-label: main
timeout: 10
force-pull: true
native:
search-locations: classpath:/shared
management:
endpoints:
web:
exposure:
include: health,info,refresh,bus-refresh
endpoint:
health:
show-details: always
encrypt:
key: ${CONFIG_ENCRYPT_KEY:default-encryption-key}
Client Configuration
Client Dependencies
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies>
bootstrap.yml - Client
spring:
application:
name: user-service
profiles:
active: ${SPRING_PROFILES_ACTIVE:development}
cloud:
config:
uri: http://config-server:8888
username: admin
password: ${CONFIG_SERVER_PASSWORD:secret}
fail-fast: true
retry:
initial-interval: 1000
max-interval: 2000
max-attempts: 6
Configuration Client Class
@RestController
@RefreshScope
public class UserController {
@Value("${app.feature.flags.new-registration:false}")
private boolean newRegistrationFeature;
@Value("${app.database.connection.pool.size:10}")
private int connectionPoolSize;
@Value("${app.notification.email.enabled:true}")
private boolean emailEnabled;
@Autowired
private ApplicationConfig applicationConfig;
@GetMapping("/config")
public Map<String, Object> getConfig() {
return Map.of(
"newRegistrationFeature", newRegistrationFeature,
"connectionPoolSize", connectionPoolSize,
"emailEnabled", emailEnabled,
"maxUsers", applicationConfig.getMaxUsers()
);
}
}
@Component
@ConfigurationProperties(prefix = "app")
public class ApplicationConfig {
private int maxUsers = 1000;
private RateLimit rateLimit = new RateLimit();
private Cache cache = new Cache();
// Getters and setters
public static class RateLimit {
private int requestsPerMinute = 100;
private boolean enabled = true;
// Getters and setters
}
public static class Cache {
private int ttlSeconds = 300;
private int maxSize = 10000;
// Getters and setters
}
}
2. Consul for Configuration Management
Consul Client Setup
Dependencies
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-config</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> </dependencies>
Consul Configuration
@Configuration
public class ConsulConfig {
@Bean
public ConsulConfigProperties consulConfigProperties() {
ConsulConfigProperties properties = new ConsulConfigProperties();
properties.setFailFast(true);
properties.setPrefix("config");
return properties;
}
}
bootstrap.yml for Consul
spring:
application:
name: order-service
cloud:
consul:
host: ${CONSUL_HOST:localhost}
port: ${CONSUL_PORT:8500}
config:
enabled: true
format: YAML
prefix: config
default-context: application
profile-separator: '-'
data-key: data
watch:
wait-time: 55
enabled: true
discovery:
instance-id: ${spring.application.name}:${random.value}
health-check-path: /actuator/health
health-check-interval: 30s
Consul Configuration Watcher
@Component
public class ConsulConfigWatcher {
private static final Logger logger = LoggerFactory.getLogger(ConsulConfigWatcher.class);
@Autowired
private ConfigurableApplicationContext applicationContext;
@EventListener
public void handleConfigUpdate(EnvironmentChangeEvent event) {
logger.info("Configuration changed: {}", event.getKeys());
// Refresh specific beans
applicationContext.getBeanFactory().destroySingleton("refreshableBean");
// Perform custom logic based on changed keys
event.getKeys().forEach(key -> {
switch (key) {
case "app.feature.flags.new-payment":
logger.info("New payment feature flag updated");
break;
case "app.database.connection.pool.size":
logger.info("Database connection pool size updated");
break;
}
});
}
}
3. ZooKeeper Configuration Management
ZooKeeper Setup
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zookeeper-config</artifactId> </dependency> </dependencies>
ZooKeeper Configuration Client
@Configuration
public class ZookeeperConfig {
@Bean
@ConditionalOnMissingBean
public CuratorFramework curatorFramework(ZookeeperConfigProperties properties) {
CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder();
RetryPolicy retryPolicy = new ExponentialBackoffRetry(
properties.getBaseSleepTimeMs(),
properties.getMaxRetries()
);
CuratorFramework curator = builder
.connectString(properties.getConnectString())
.retryPolicy(retryPolicy)
.namespace(properties.getNamespace())
.build();
curator.start();
return curator;
}
}
@Component
public class ZookeeperConfigManager {
private final CuratorFramework curator;
private final ObjectMapper objectMapper;
public ZookeeperConfigManager(CuratorFramework curator, ObjectMapper objectMapper) {
this.curator = curator;
this.objectMapper = objectMapper;
}
public void saveConfig(String path, Object config) throws Exception {
byte[] data = objectMapper.writeValueAsBytes(config);
if (curator.checkExists().forPath(path) == null) {
curator.create().creatingParentsIfNeeded().forPath(path, data);
} else {
curator.setData().forPath(path, data);
}
}
public <T> T getConfig(String path, Class<T> clazz) throws Exception {
if (curator.checkExists().forPath(path) == null) {
return null;
}
byte[] data = curator.getData().forPath(path);
return objectMapper.readValue(data, clazz);
}
public void watchConfig(String path, ConfigWatcher watcher) throws Exception {
NodeCache nodeCache = new NodeCache(curator, path);
nodeCache.getListenable().addListener(() -> {
ChildData currentData = nodeCache.getCurrentData();
if (currentData != null) {
byte[] data = currentData.getData();
watcher.onConfigChanged(data);
}
});
nodeCache.start();
}
public interface ConfigWatcher {
void onConfigChanged(byte[] newData);
}
}
4. Environment-Specific Configuration
Multi-Environment Setup
application-development.yml
app: name: "User Service - Development" environment: "development" database: url: "jdbc:postgresql://localhost:5432/users_dev" username: "dev_user" pool-size: 5 feature: flags: new-registration: true email-verification: false cache: enabled: true ttl: 300 rate-limit: enabled: false requests-per-minute: 1000 logging: level: com.example: DEBUG org.springframework: INFO
application-production.yml
app:
name: "User Service - Production"
environment: "production"
database:
url: "jdbc:postgresql://prod-db-cluster:5432/users_prod"
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
pool-size: 20
feature:
flags:
new-registration: true
email-verification: true
cache:
enabled: true
ttl: 900
rate-limit:
enabled: true
requests-per-minute: 100
logging:
level:
com.example: INFO
org.springframework: WARN
management:
endpoints:
web:
exposure:
include: health,metrics,info
endpoint:
health:
show-details: when_authorized
5. Dynamic Configuration with Feature Flags
Feature Flag Management
@Component
@RefreshScope
public class FeatureFlagManager {
@Value("${app.feature.flags.new-registration:false}")
private boolean newRegistrationEnabled;
@Value("${app.feature.flags.payment-v2:false}")
private boolean paymentV2Enabled;
@Value("${app.feature.flags.experimental-ui:false}")
private boolean experimentalUiEnabled;
private final Map<String, Boolean> featureFlags = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
featureFlags.put("new-registration", newRegistrationEnabled);
featureFlags.put("payment-v2", paymentV2Enabled);
featureFlags.put("experimental-ui", experimentalUiEnabled);
}
@EventListener
public void onConfigUpdate(EnvironmentChangeEvent event) {
if (event.getKeys().stream().anyMatch(key -> key.startsWith("app.feature.flags"))) {
refreshFeatureFlags();
}
}
private void refreshFeatureFlags() {
// This method will be called when configuration is refreshed
// In a real implementation, you would re-read the values
}
public boolean isEnabled(String feature) {
return featureFlags.getOrDefault(feature, false);
}
public Map<String, Boolean> getAllFlags() {
return new HashMap<>(featureFlags);
}
}
@RestController
@RequestMapping("/api/features")
public class FeatureFlagController {
private final FeatureFlagManager featureFlagManager;
public FeatureFlagController(FeatureFlagManager featureFlagManager) {
this.featureFlagManager = featureFlagManager;
}
@GetMapping
public Map<String, Boolean> getFeatureFlags() {
return featureFlagManager.getAllFlags();
}
@GetMapping("/{feature}")
public boolean isFeatureEnabled(@PathVariable String feature) {
return featureFlagManager.isEnabled(feature);
}
}
6. Configuration Encryption
Jasypt Encryption Setup
<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.5</version> </dependency>
Encryption Configuration
@Component
public class ConfigurationEncryptor {
@Value("${jasypt.encryptor.password}")
private String encryptionPassword;
public String encrypt(String value) {
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
encryptor.setPassword(encryptionPassword);
return "ENC(" + encryptor.encrypt(value) + ")";
}
public String decrypt(String encryptedValue) {
if (encryptedValue.startsWith("ENC(") && encryptedValue.endsWith(")")) {
String actualValue = encryptedValue.substring(4, encryptedValue.length() - 1);
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
encryptor.setPassword(encryptionPassword);
return encryptor.decrypt(actualValue);
}
return encryptedValue;
}
}
@Configuration
public class EncryptionConfig {
@Bean
public StringEncryptor jasyptStringEncryptor() {
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
config.setPassword(System.getenv("JASYPT_ENCRYPTOR_PASSWORD"));
config.setAlgorithm("PBEWITHHMACSHA512ANDAES_256");
config.setKeyObtentionIterations("1000");
config.setPoolSize("1");
config.setProviderName("SunJCE");
config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator");
config.setStringOutputType("base64");
encryptor.setConfig(config);
return encryptor;
}
}
Encrypted Properties Example
# application.yml with encrypted values app: database: password: "ENC(AbCdEfGhIjKlMnOpQrStUvWxYz123456==)" api: key: "ENC(XyZaBcDeFgHiJkLmNoPqRsTuVwXyZ987==)" external: service: secret: "ENC(MnOpQrStUvWxYzAbCdEfGhIjKlMnOp12==)" jasypt: encryptor: bean: jasyptStringEncryptor
7. Configuration Validation
Configuration Validation Setup
@Configuration
@ConfigurationProperties(prefix = "app")
@Validated
public class AppConfig {
@NotNull
@NotEmpty
private String name;
@Valid
private DatabaseConfig database;
@Valid
private SecurityConfig security;
@Min(1)
@Max(100)
private int maxConnections;
// Getters and setters
@AssertTrue(message = "Database configuration must be valid")
public boolean isDatabaseConfigValid() {
return database != null && database.isValid();
}
public static class DatabaseConfig {
@NotBlank
private String url;
@NotBlank
private String username;
private String password;
@Min(1)
@Max(50)
private int poolSize;
public boolean isValid() {
return url != null && !url.trim().isEmpty();
}
// Getters and setters
}
public static class SecurityConfig {
private boolean sslEnabled;
@Pattern(regexp = "^(TLSv1\\.2|TLSv1\\.3)$")
private String sslProtocol = "TLSv1.2";
// Getters and setters
}
}
@Component
public class ConfigValidator {
private final Validator validator;
public ConfigValidator(Validator validator) {
this.validator = validator;
}
public void validateConfig(AppConfig config) {
Set<ConstraintViolation<AppConfig>> violations = validator.validate(config);
if (!violations.isEmpty()) {
List<String> errorMessages = violations.stream()
.map(v -> v.getPropertyPath() + ": " + v.getMessage())
.collect(Collectors.toList());
throw new IllegalStateException("Configuration validation failed: " +
String.join(", ", errorMessages));
}
}
@EventListener
public void validateOnRefresh(EnvironmentChangeEvent event) {
// Re-validate configuration on refresh
}
}
8. Distributed Configuration Best Practices
Configuration Health Check
@Component
public class ConfigurationHealthIndicator implements HealthIndicator {
private final Environment environment;
private final AppConfig appConfig;
public ConfigurationHealthIndicator(Environment environment, AppConfig appConfig) {
this.environment = environment;
this.appConfig = appConfig;
}
@Override
public Health health() {
try {
// Check if required properties are set
checkRequiredProperties();
// Validate configuration
validateConfiguration();
return Health.up()
.withDetail("configServer", "connected")
.withDetail("propertiesLoaded", true)
.build();
} catch (Exception e) {
return Health.down(e)
.withDetail("error", "Configuration validation failed")
.build();
}
}
private void checkRequiredProperties() {
String[] requiredProps = {
"app.database.url",
"app.database.username",
"spring.application.name"
};
for (String prop : requiredProps) {
if (!environment.containsProperty(prop)) {
throw new IllegalStateException("Required property missing: " + prop);
}
}
}
private void validateConfiguration() {
// Custom validation logic
if (appConfig.getMaxConnections() < 1) {
throw new IllegalStateException("maxConnections must be at least 1");
}
}
}
Configuration Monitoring
@Component
public class ConfigurationMetrics {
private final MeterRegistry meterRegistry;
private final Environment environment;
private final Counter configUpdateCounter;
private final Gauge configValueGauge;
public ConfigurationMetrics(MeterRegistry meterRegistry, Environment environment) {
this.meterRegistry = meterRegistry;
this.environment = environment;
this.configUpdateCounter = Counter.builder("config.updates")
.description("Number of configuration updates")
.register(meterRegistry);
// Register gauges for important configuration values
setupConfigurationGauges();
}
@EventListener
public void onConfigUpdate(EnvironmentChangeEvent event) {
configUpdateCounter.increment();
// Update gauges with new values
updateConfigurationGauges();
}
private void setupConfigurationGauges() {
Gauge.builder("config.database.pool.size")
.description("Database connection pool size")
.tag("application", environment.getProperty("spring.application.name"))
.register(meterRegistry, this, metrics ->
Integer.parseInt(environment.getProperty("app.database.pool.size", "10")));
}
private void updateConfigurationGauges() {
// Force gauge refresh
meterRegistry.forEachMeter(Meter::measure);
}
}
This comprehensive guide covers distributed configuration management in Java using various tools and patterns, providing a solid foundation for managing configuration in distributed systems effectively.