Dynamic Config Updates in Java: Comprehensive Real-time Configuration Management

Dynamic configuration updates allow applications to modify their behavior at runtime without requiring restarts. This guide covers multiple approaches for implementing dynamic configuration in Java applications.


Core Concepts

What are Dynamic Config Updates?

  • Real-time configuration changes without application restart
  • Hot-reloading of properties and settings
  • Runtime feature flag toggling
  • Dynamic behavior modification

Key Benefits:

  • Zero Downtime: Update configurations without restarting
  • A/B Testing: Enable feature flags dynamically
  • Emergency Controls: Quickly disable features during incidents
  • Personalization: User-specific configuration changes

Architecture Overview

Configuration Sources
├── File System
├── Database
├── Config Server (Spring Cloud Config)
├── Environment Variables
└── Feature Flag Services
↓
Configuration Manager
↓
Application Components
├── Feature Toggles
├── Business Logic
├── API Endpoints
└── Scheduled Tasks

Dependencies and Setup

Maven Dependencies
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<spring-cloud.version>2022.0.3</spring-cloud.version>
<micrometer.version>1.11.5</micrometer.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Spring Cloud Config -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Redis for Caching -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Monitoring -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>${micrometer.version}</version>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Core Implementation

1. Configuration Models
@Entity
@Table(name = "dynamic_configs")
@Data
public class DynamicConfig {
@Id
private String key;
@Column(nullable = false)
private String value;
@Column(name = "config_type")
@Enumerated(EnumType.STRING)
private ConfigType type = ConfigType.STRING;
private String description;
@Column(name = "is_encrypted")
private boolean encrypted = false;
@Column(name = "is_sensitive")
private boolean sensitive = false;
@CreationTimestamp
private Instant createdAt;
@UpdateTimestamp
private Instant updatedAt;
public Object getTypedValue() {
return type.convert(value);
}
}
@Entity
@Table(name = "feature_flags")
@Data
public class FeatureFlag {
@Id
private String name;
@Column(nullable = false)
private boolean enabled = false;
private String description;
@Column(name = "target_users")
private String targetUsers; // JSON array of user IDs
@Column(name = "rollout_percentage")
private int rolloutPercentage = 100;
@ElementCollection
@CollectionTable(name = "feature_flag_metadata", joinColumns = @JoinColumn(name = "feature_name"))
@MapKeyColumn(name = "meta_key")
@Column(name = "meta_value")
private Map<String, String> metadata = new HashMap<>();
@CreationTimestamp
private Instant createdAt;
@UpdateTimestamp
private Instant updatedAt;
public boolean isEnabledForUser(String userId) {
if (!enabled) return false;
if (rolloutPercentage < 100) {
// Simple hash-based rollout
int userHash = Math.abs(userId.hashCode()) % 100;
return userHash < rolloutPercentage;
}
if (targetUsers != null && !targetUsers.isEmpty()) {
try {
List<String> targetUserList = new ObjectMapper().readValue(targetUsers, List.class);
return targetUserList.contains(userId);
} catch (Exception e) {
// Log error and fall back to enabled flag
}
}
return enabled;
}
}
public enum ConfigType {
STRING {
@Override
public Object convert(String value) {
return value;
}
},
INTEGER {
@Override
public Object convert(String value) {
return Integer.parseInt(value);
}
},
BOOLEAN {
@Override
public Object convert(String value) {
return Boolean.parseBoolean(value);
}
},
DOUBLE {
@Override
public Object convert(String value) {
return Double.parseDouble(value);
}
},
JSON {
@Override
public Object convert(String value) {
try {
return new ObjectMapper().readValue(value, Map.class);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid JSON value: " + value, e);
}
}
};
public abstract Object convert(String value);
}
2. Configuration Manager
@Service
@Slf4j
public class DynamicConfigManager {
private final DynamicConfigRepository configRepository;
private final FeatureFlagRepository featureFlagRepository;
private final RedisTemplate<String, Object> redisTemplate;
private final ApplicationEventPublisher eventPublisher;
private final Map<String, Object> configCache = new ConcurrentHashMap<>();
private final Map<String, FeatureFlag> featureFlagCache = new ConcurrentHashMap<>();
private final AtomicReference<Instant> lastConfigUpdate = new AtomicReference<>(Instant.now());
private final ScheduledExecutorService configReloadExecutor;
private final long configReloadInterval = 30000; // 30 seconds
public DynamicConfigManager(DynamicConfigRepository configRepository,
FeatureFlagRepository featureFlagRepository,
RedisTemplate<String, Object> redisTemplate,
ApplicationEventPublisher eventPublisher) {
this.configRepository = configRepository;
this.featureFlagRepository = featureFlagRepository;
this.redisTemplate = redisTemplate;
this.eventPublisher = eventPublisher;
this.configReloadExecutor = Executors.newSingleThreadScheduledExecutor(
r -> new Thread(r, "config-reloader"));
initializeConfigurations();
startConfigReloader();
}
@PostConstruct
public void initializeConfigurations() {
log.info("Initializing dynamic configurations");
reloadAllConfigurations();
}
// Configuration Methods
public String getConfig(String key) {
return getConfig(key, null);
}
public String getConfig(String key, String defaultValue) {
// Check cache first
Object value = configCache.get(key);
if (value instanceof String) {
return (String) value;
}
// Check database
Optional<DynamicConfig> config = configRepository.findById(key);
if (config.isPresent()) {
String configValue = config.get().getValue();
configCache.put(key, configValue);
return configValue;
}
return defaultValue;
}
public <T> T getConfig(String key, Class<T> type) {
return getConfig(key, type, null);
}
public <T> T getConfig(String key, Class<T> type, T defaultValue) {
Object value = configCache.get(key);
if (value != null && type.isInstance(value)) {
return type.cast(value);
}
Optional<DynamicConfig> config = configRepository.findById(key);
if (config.isPresent()) {
Object typedValue = config.get().getTypedValue();
if (type.isInstance(typedValue)) {
configCache.put(key, typedValue);
return type.cast(typedValue);
}
}
return defaultValue;
}
public void setConfig(String key, String value, ConfigType type) {
DynamicConfig config = new DynamicConfig();
config.setKey(key);
config.setValue(value);
config.setType(type);
setConfig(config);
}
public void setConfig(DynamicConfig config) {
DynamicConfig saved = configRepository.save(config);
configCache.put(saved.getKey(), saved.getTypedValue());
lastConfigUpdate.set(Instant.now());
// Publish configuration change event
eventPublisher.publishEvent(new ConfigChangedEvent(this, saved.getKey(), saved.getTypedValue()));
log.info("Configuration updated: {} = {}", saved.getKey(), 
saved.isSensitive() ? "***" : saved.getValue());
}
public void deleteConfig(String key) {
configRepository.deleteById(key);
configCache.remove(key);
lastConfigUpdate.set(Instant.now());
eventPublisher.publishEvent(new ConfigDeletedEvent(this, key));
log.info("Configuration deleted: {}", key);
}
// Feature Flag Methods
public boolean isFeatureEnabled(String featureName) {
return isFeatureEnabled(featureName, null);
}
public boolean isFeatureEnabled(String featureName, String userId) {
FeatureFlag flag = featureFlagCache.get(featureName);
if (flag == null) {
flag = featureFlagRepository.findById(featureName).orElse(null);
if (flag != null) {
featureFlagCache.put(featureName, flag);
} else {
return false; // Feature doesn't exist
}
}
if (userId != null) {
return flag.isEnabledForUser(userId);
}
return flag.isEnabled();
}
public void setFeatureFlag(FeatureFlag featureFlag) {
FeatureFlag saved = featureFlagRepository.save(featureFlag);
featureFlagCache.put(saved.getName(), saved);
eventPublisher.publishEvent(new FeatureFlagChangedEvent(this, saved));
log.info("Feature flag updated: {} = {}", saved.getName(), saved.isEnabled());
}
public void setFeatureEnabled(String featureName, boolean enabled) {
FeatureFlag flag = featureFlagRepository.findById(featureName)
.orElse(new FeatureFlag());
flag.setName(featureName);
flag.setEnabled(enabled);
setFeatureFlag(flag);
}
// Bulk Operations
public Map<String, Object> getAllConfigs() {
return new HashMap<>(configCache);
}
public Map<String, FeatureFlag> getAllFeatureFlags() {
return new HashMap<>(featureFlagCache);
}
public void reloadAllConfigurations() {
log.info("Reloading all configurations from database");
// Reload configurations
List<DynamicConfig> configs = configRepository.findAll();
configCache.clear();
configs.forEach(config -> 
configCache.put(config.getKey(), config.getTypedValue()));
// Reload feature flags
List<FeatureFlag> flags = featureFlagRepository.findAll();
featureFlagCache.clear();
flags.forEach(flag -> featureFlagCache.put(flag.getName(), flag));
lastConfigUpdate.set(Instant.now());
log.info("Configurations reloaded: {} configs, {} feature flags", 
configs.size(), flags.size());
}
// Cache Management
public void clearCache() {
configCache.clear();
featureFlagCache.clear();
reloadAllConfigurations();
}
public void clearCache(String key) {
configCache.remove(key);
featureFlagCache.remove(key);
}
// Monitoring
public ConfigManagerMetrics getMetrics() {
return new ConfigManagerMetrics(
configCache.size(),
featureFlagCache.size(),
lastConfigUpdate.get(),
Instant.now()
);
}
private void startConfigReloader() {
configReloadExecutor.scheduleAtFixedRate(() -> {
try {
reloadAllConfigurations();
} catch (Exception e) {
log.error("Failed to reload configurations", e);
}
}, configReloadInterval, configReloadInterval, TimeUnit.MILLISECONDS);
}
@PreDestroy
public void shutdown() {
log.info("Shutting down DynamicConfigManager");
configReloadExecutor.shutdown();
try {
if (!configReloadExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
configReloadExecutor.shutdownNow();
}
} catch (InterruptedException e) {
configReloadExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
@Data
class ConfigManagerMetrics {
private final int configCount;
private final int featureFlagCount;
private final Instant lastUpdate;
private final Instant currentTime;
public long getSecondsSinceLastUpdate() {
return Duration.between(lastUpdate, currentTime).getSeconds();
}
}
3. Configuration Events
public abstract class ConfigEvent extends ApplicationEvent {
private final String configKey;
private final Instant timestamp;
public ConfigEvent(Object source, String configKey) {
super(source);
this.configKey = configKey;
this.timestamp = Instant.now();
}
public String getConfigKey() { return configKey; }
public Instant getTimestamp() { return timestamp; }
}
public class ConfigChangedEvent extends ConfigEvent {
private final Object newValue;
public ConfigChangedEvent(Object source, String configKey, Object newValue) {
super(source, configKey);
this.newValue = newValue;
}
public Object getNewValue() { return newValue; }
}
public class ConfigDeletedEvent extends ConfigEvent {
public ConfigDeletedEvent(Object source, String configKey) {
super(source, configKey);
}
}
public class FeatureFlagChangedEvent extends ApplicationEvent {
private final FeatureFlag featureFlag;
private final Instant timestamp;
public FeatureFlagChangedEvent(Object source, FeatureFlag featureFlag) {
super(source);
this.featureFlag = featureFlag;
this.timestamp = Instant.now();
}
public FeatureFlag getFeatureFlag() { return featureFlag; }
public Instant getTimestamp() { return timestamp; }
}
4. Event Listeners
@Component
@Slf4j
public class ConfigChangeListener {
private final DynamicConfigManager configManager;
private final MeterRegistry meterRegistry;
public ConfigChangeListener(DynamicConfigManager configManager,
MeterRegistry meterRegistry) {
this.configManager = configManager;
this.meterRegistry = meterRegistry;
}
@EventListener
public void handleConfigChanged(ConfigChangedEvent event) {
log.info("Configuration changed: {} = {}", event.getConfigKey(), event.getNewValue());
// Update metrics
meterRegistry.counter("config.changes", 
"key", event.getConfigKey()).increment();
// Handle specific configuration changes
switch (event.getConfigKey()) {
case "app.rate.limit.requests":
handleRateLimitChange(event);
break;
case "app.cache.ttl":
handleCacheTtlChange(event);
break;
case "app.feature.new-ui":
handleNewUiFeatureChange(event);
break;
}
}
@EventListener
public void handleConfigDeleted(ConfigDeletedEvent event) {
log.info("Configuration deleted: {}", event.getConfigKey());
meterRegistry.counter("config.deletes", 
"key", event.getConfigKey()).increment();
}
@EventListener
public void handleFeatureFlagChanged(FeatureFlagChangedEvent event) {
FeatureFlag flag = event.getFeatureFlag();
log.info("Feature flag changed: {} = {} (rollout: {}%)", 
flag.getName(), flag.isEnabled(), flag.getRolloutPercentage());
meterRegistry.counter("featureflag.changes",
"feature", flag.getName(),
"enabled", String.valueOf(flag.isEnabled())).increment();
// Handle specific feature flag changes
switch (flag.getName()) {
case "experimental-payment":
handleExperimentalPaymentFeature(flag);
break;
case "dark-mode":
handleDarkModeFeature(flag);
break;
}
}
private void handleRateLimitChange(ConfigChangedEvent event) {
Integer newLimit = (Integer) event.getNewValue();
log.info("Rate limit changed to: {} requests per minute", newLimit);
// Update rate limiter component
}
private void handleCacheTtlChange(ConfigChangedEvent event) {
Integer newTtl = (Integer) event.getNewValue();
log.info("Cache TTL changed to: {} seconds", newTtl);
// Update cache configuration
}
private void handleNewUiFeatureChange(ConfigChangedEvent event) {
Boolean enabled = (Boolean) event.getNewValue();
log.info("New UI feature is now: {}", enabled ? "ENABLED" : "DISABLED");
// Notify UI components
}
private void handleExperimentalPaymentFeature(FeatureFlag flag) {
log.info("Experimental payment feature updated - enabled: {}, rollout: {}%",
flag.isEnabled(), flag.getRolloutPercentage());
// Update payment service configuration
}
private void handleDarkModeFeature(FeatureFlag flag) {
log.info("Dark mode feature updated - enabled: {}, target users: {}",
flag.isEnabled(), flag.getTargetUsers());
// Notify frontend services
}
}
5. Repository Interfaces
@Repository
public interface DynamicConfigRepository extends JpaRepository<DynamicConfig, String> {
@Query("SELECT c FROM DynamicConfig c WHERE c.sensitive = false")
List<DynamicConfig> findAllNonSensitive();
@Query("SELECT c FROM DynamicConfig c WHERE c.type = :type")
List<DynamicConfig> findByType(@Param("type") ConfigType type);
@Modifying
@Query("UPDATE DynamicConfig c SET c.value = :value, c.updatedAt = CURRENT_TIMESTAMP WHERE c.key = :key")
void updateValue(@Param("key") String key, @Param("value") String value);
}
@Repository
public interface FeatureFlagRepository extends JpaRepository<FeatureFlag, String> {
List<FeatureFlag> findByEnabledTrue();
@Query("SELECT f FROM FeatureFlag f WHERE f.enabled = true AND f.rolloutPercentage = 100")
List<FeatureFlag> findFullyEnabledFlags();
@Modifying
@Query("UPDATE FeatureFlag f SET f.enabled = :enabled, f.updatedAt = CURRENT_TIMESTAMP WHERE f.name = :name")
void updateEnabledStatus(@Param("name") String name, @Param("enabled") boolean enabled);
}

Spring Cloud Config Integration

1. Spring Cloud Config Client with Dynamic Updates
@Service
@Slf4j
public class CloudConfigDynamicManager {
private final DynamicConfigManager dynamicConfigManager;
private final ContextRefresher contextRefresher;
private final Environment environment;
public CloudConfigDynamicManager(DynamicConfigManager dynamicConfigManager,
ContextRefresher contextRefresher,
Environment environment) {
this.dynamicConfigManager = dynamicConfigManager;
this.contextRefresher = contextRefresher;
this.environment = environment;
}
@EventListener
public void handleCloudConfigRefresh(EnvironmentChangeEvent event) {
log.info("Cloud Config refresh detected. Changed keys: {}", event.getKeys());
// Sync cloud config changes to dynamic config manager
event.getKeys().forEach(key -> {
String value = environment.getProperty(key);
if (value != null) {
ConfigType type = inferConfigType(value);
dynamicConfigManager.setConfig(key, value, type);
}
});
}
public void refreshFromCloudConfig() {
try {
Set<String> refreshedKeys = contextRefresher.refresh();
log.info("Refreshed {} configuration keys from Cloud Config", refreshedKeys.size());
} catch (Exception e) {
log.error("Failed to refresh configuration from Cloud Config", e);
}
}
private ConfigType inferConfigType(String value) {
if (value == null) return ConfigType.STRING;
if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
return ConfigType.BOOLEAN;
}
try {
Integer.parseInt(value);
return ConfigType.INTEGER;
} catch (NumberFormatException e) {
// Not an integer
}
try {
Double.parseDouble(value);
return ConfigType.DOUBLE;
} catch (NumberFormatException e) {
// Not a double
}
if (value.startsWith("{") && value.endsWith("}")) {
return ConfigType.JSON;
}
return ConfigType.STRING;
}
}
2. Refresh Scope Configuration
@Configuration
@Slf4j
public class DynamicConfigConfiguration {
@Bean
@RefreshScope
public DatabaseConfig databaseConfig(DynamicConfigManager configManager) {
return new DatabaseConfig(
configManager.getConfig("app.database.url", ""),
configManager.getConfig("app.database.username", ""),
configManager.getConfig("app.database.password", ""),
configManager.getConfig("app.database.max-pool-size", Integer.class, 10)
);
}
@Bean
@RefreshScope
public RateLimiterConfig rateLimiterConfig(DynamicConfigManager configManager) {
return new RateLimiterConfig(
configManager.getConfig("app.rate.limit.requests", Integer.class, 100),
configManager.getConfig("app.rate.limit.duration", Integer.class, 60)
);
}
}
@Data
class DatabaseConfig {
private final String url;
private final String username;
private final String password;
private final int maxPoolSize;
}
@Data
class RateLimiterConfig {
private final int maxRequests;
private final int durationSeconds;
public Duration getDuration() {
return Duration.ofSeconds(durationSeconds);
}
}

REST API Controllers

1. Configuration Management API
@RestController
@RequestMapping("/api/config")
@Slf4j
@Validated
public class ConfigController {
private final DynamicConfigManager configManager;
private final CloudConfigDynamicManager cloudConfigManager;
public ConfigController(DynamicConfigManager configManager,
CloudConfigDynamicManager cloudConfigManager) {
this.configManager = configManager;
this.cloudConfigManager = cloudConfigManager;
}
@GetMapping
public ResponseEntity<Map<String, Object>> getAllConfigs(
@RequestParam(defaultValue = "false") boolean includeSensitive) {
Map<String, Object> configs = configManager.getAllConfigs();
if (!includeSensitive) {
// Filter out sensitive values in response
configs.replaceAll((k, v) -> "***");
}
Map<String, Object> response = new HashMap<>();
response.put("configs", configs);
response.put("lastUpdate", configManager.getMetrics().getLastUpdate());
response.put("totalCount", configs.size());
return ResponseEntity.ok(response);
}
@GetMapping("/{key}")
public ResponseEntity<ConfigResponse> getConfig(@PathVariable String key) {
Object value = configManager.getAllConfigs().get(key);
if (value == null) {
return ResponseEntity.notFound().build();
}
ConfigResponse response = new ConfigResponse(key, value, configManager.getMetrics().getLastUpdate());
return ResponseEntity.ok(response);
}
@PostMapping
public ResponseEntity<ConfigResponse> setConfig(@RequestBody @Valid ConfigUpdateRequest request) {
configManager.setConfig(request.getKey(), request.getValue(), request.getType());
Object value = configManager.getConfig(request.getKey());
ConfigResponse response = new ConfigResponse(request.getKey(), value, Instant.now());
return ResponseEntity.ok(response);
}
@DeleteMapping("/{key}")
public ResponseEntity<Void> deleteConfig(@PathVariable String key) {
configManager.deleteConfig(key);
return ResponseEntity.noContent().build();
}
@PostMapping("/refresh")
public ResponseEntity<RefreshResponse> refreshConfigs() {
configManager.reloadAllConfigurations();
RefreshResponse response = new RefreshResponse(
true,
"Configurations refreshed successfully",
configManager.getMetrics().getLastUpdate(),
configManager.getMetrics().getConfigCount()
);
return ResponseEntity.ok(response);
}
@PostMapping("/refresh/cloud")
public ResponseEntity<RefreshResponse> refreshCloudConfig() {
cloudConfigManager.refreshFromCloudConfig();
RefreshResponse response = new RefreshResponse(
true,
"Cloud Config refresh initiated",
Instant.now(),
configManager.getMetrics().getConfigCount()
);
return ResponseEntity.ok(response);
}
@GetMapping("/metrics")
public ResponseEntity<ConfigManagerMetrics> getMetrics() {
return ResponseEntity.ok(configManager.getMetrics());
}
}
@Data
class ConfigUpdateRequest {
@NotBlank
private String key;
@NotBlank
private String value;
private ConfigType type = ConfigType.STRING;
private String description;
private boolean sensitive = false;
}
@Data
class ConfigResponse {
private final String key;
private final Object value;
private final Instant lastUpdated;
}
@Data
class RefreshResponse {
private final boolean success;
private final String message;
private final Instant refreshedAt;
private final int configCount;
}
2. Feature Flag API
@RestController
@RequestMapping("/api/features")
@Slf4j
@Validated
public class FeatureFlagController {
private final DynamicConfigManager configManager;
public FeatureFlagController(DynamicConfigManager configManager) {
this.configManager = configManager;
}
@GetMapping
public ResponseEntity<FeatureFlagsResponse> getAllFeatures() {
Map<String, FeatureFlag> flags = configManager.getAllFeatureFlags();
FeatureFlagsResponse response = new FeatureFlagsResponse(
flags,
Instant.now(),
flags.size()
);
return ResponseEntity.ok(response);
}
@GetMapping("/{featureName}")
public ResponseEntity<FeatureStatusResponse> getFeatureStatus(
@PathVariable String featureName,
@RequestParam(required = false) String userId) {
boolean enabled = configManager.isFeatureEnabled(featureName, userId);
FeatureFlag flag = configManager.getAllFeatureFlags().get(featureName);
if (flag == null) {
return ResponseEntity.notFound().build();
}
FeatureStatusResponse response = new FeatureStatusResponse(
featureName,
enabled,
flag,
Instant.now()
);
return ResponseEntity.ok(response);
}
@PostMapping("/{featureName}/enable")
public ResponseEntity<FeatureStatusResponse> enableFeature(
@PathVariable String featureName,
@RequestParam(defaultValue = "100") int rolloutPercentage) {
FeatureFlag flag = configManager.getAllFeatureFlags().get(featureName);
if (flag == null) {
flag = new FeatureFlag();
flag.setName(featureName);
}
flag.setEnabled(true);
flag.setRolloutPercentage(rolloutPercentage);
configManager.setFeatureFlag(flag);
FeatureStatusResponse response = new FeatureStatusResponse(
featureName,
true,
flag,
Instant.now()
);
return ResponseEntity.ok(response);
}
@PostMapping("/{featureName}/disable")
public ResponseEntity<FeatureStatusResponse> disableFeature(@PathVariable String featureName) {
configManager.setFeatureEnabled(featureName, false);
FeatureFlag flag = configManager.getAllFeatureFlags().get(featureName);
FeatureStatusResponse response = new FeatureStatusResponse(
featureName,
false,
flag,
Instant.now()
);
return ResponseEntity.ok(response);
}
@PostMapping("/{featureName}/target")
public ResponseEntity<FeatureStatusResponse> setFeatureTarget(
@PathVariable String featureName,
@RequestBody FeatureTargetRequest request) {
FeatureFlag flag = configManager.getAllFeatureFlags().get(featureName);
if (flag == null) {
flag = new FeatureFlag();
flag.setName(featureName);
}
flag.setEnabled(request.isEnabled());
flag.setTargetUsers(request.getTargetUsers());
flag.setRolloutPercentage(request.getRolloutPercentage());
configManager.setFeatureFlag(flag);
FeatureStatusResponse response = new FeatureStatusResponse(
featureName,
flag.isEnabled(),
flag,
Instant.now()
);
return ResponseEntity.ok(response);
}
}
@Data
class FeatureFlagsResponse {
private final Map<String, FeatureFlag> features;
private final Instant lastUpdated;
private final int totalCount;
}
@Data
class FeatureStatusResponse {
private final String featureName;
private final boolean enabled;
private final FeatureFlag details;
private final Instant checkedAt;
}
@Data
class FeatureTargetRequest {
private boolean enabled = true;
private String targetUsers; // JSON array
private int rolloutPercentage = 100;
}

Business Logic with Dynamic Configuration

1. Feature-Toggle Service
@Service
@Slf4j
public class FeatureToggleService {
private final DynamicConfigManager configManager;
public FeatureToggleService(DynamicConfigManager configManager) {
this.configManager = configManager;
}
public boolean isNewPaymentGatewayEnabled(String userId) {
return configManager.isFeatureEnabled("new-payment-gateway", userId);
}
public boolean isDarkModeEnabled(String userId) {
return configManager.isFeatureEnabled("dark-mode", userId);
}
public boolean isExperimentalSearchEnabled(String userId) {
return configManager.isFeatureEnabled("experimental-search", userId);
}
public boolean isMaintenanceModeEnabled() {
return configManager.isFeatureEnabled("maintenance-mode");
}
public Map<String, Boolean> getUserFeatures(String userId) {
Map<String, Boolean> features = new HashMap<>();
Map<String, FeatureFlag> allFlags = configManager.getAllFeatureFlags();
allFlags.forEach((name, flag) -> {
features.put(name, flag.isEnabledForUser(userId));
});
return features;
}
}
2. Rate Limiter with Dynamic Configuration
@Service
@Slf4j
public class DynamicRateLimiter {
private final DynamicConfigManager configManager;
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
public DynamicRateLimiter(DynamicConfigManager configManager) {
this.configManager = configManager;
setupConfigListeners();
}
public boolean allowRequest(String clientId) {
int maxRequests = configManager.getConfig("app.rate.limit.requests", Integer.class, 100);
int durationSeconds = configManager.getConfig("app.rate.limit.duration", Integer.class, 60);
RateLimiter limiter = limiters.computeIfAbsent(clientId, 
id -> RateLimiter.create(maxRequests, Duration.ofSeconds(durationSeconds)));
return limiter.tryAcquire();
}
public boolean allowRequest(String clientId, String endpoint) {
String key = String.format("app.rate.limit.%s.requests", endpoint);
int endpointMaxRequests = configManager.getConfig(key, Integer.class, 50);
int globalMaxRequests = configManager.getConfig("app.rate.limit.requests", Integer.class, 100);
int maxRequests = Math.min(endpointMaxRequests, globalMaxRequests);
RateLimiter limiter = limiters.computeIfAbsent(clientId + ":" + endpoint,
id -> RateLimiter.create(maxRequests, Duration.ofMinutes(1)));
return limiter.tryAcquire();
}
private void setupConfigListeners() {
// React to rate limit configuration changes
// This would typically use the event system
}
}
// Simple RateLimiter implementation
class RateLimiter {
private final int maxRequests;
private final Duration duration;
private final Queue<Instant> requests = new ConcurrentLinkedQueue<>();
private RateLimiter(int maxRequests, Duration duration) {
this.maxRequests = maxRequests;
this.duration = duration;
}
public static RateLimiter create(int maxRequests, Duration duration) {
return new RateLimiter(maxRequests, duration);
}
public synchronized boolean tryAcquire() {
cleanExpiredRequests();
if (requests.size() < maxRequests) {
requests.offer(Instant.now());
return true;
}
return false;
}
private void cleanExpiredRequests() {
Instant cutoff = Instant.now().minus(duration);
while (!requests.isEmpty() && requests.peek().isBefore(cutoff)) {
requests.poll();
}
}
}
3. Dynamic Business Service
@Service
@Slf4j
public class OrderService {
private final DynamicConfigManager configManager;
private final FeatureToggleService featureToggleService;
public OrderService(DynamicConfigManager configManager,
FeatureToggleService featureToggleService) {
this.configManager = configManager;
this.featureToggleService = featureToggleService;
}
public Order createOrder(OrderRequest request, String userId) {
// Check maintenance mode
if (featureToggleService.isMaintenanceModeEnabled()) {
throw new MaintenanceModeException("System is under maintenance");
}
// Check rate limits
// Apply dynamic pricing
double priceMultiplier = configManager.getConfig("app.pricing.multiplier", Double.class, 1.0);
double finalPrice = request.getAmount() * priceMultiplier;
// Use new payment gateway if enabled for user
boolean useNewGateway = featureToggleService.isNewPaymentGatewayEnabled(userId);
PaymentGateway gateway = useNewGateway ? new NewPaymentGateway() : new LegacyPaymentGateway();
// Process payment
PaymentResult result = gateway.processPayment(finalPrice, request.getPaymentMethod());
// Create order
Order order = new Order();
order.setUserId(userId);
order.setAmount(finalPrice);
order.setPaymentGateway(useNewGateway ? "NEW" : "LEGACY");
order.setCreatedAt(Instant.now());
log.info("Order created with dynamic pricing (multiplier: {}) and gateway: {}", 
priceMultiplier, useNewGateway ? "NEW" : "LEGACY");
return order;
}
public List<Order> getOrders(String userId, OrderFilter filter) {
// Use experimental search if enabled
boolean useExperimentalSearch = featureToggleService.isExperimentalSearchEnabled(userId);
if (useExperimentalSearch) {
return experimentalOrderSearch(userId, filter);
} else {
return traditionalOrderSearch(userId, filter);
}
}
private List<Order> experimentalOrderSearch(String userId, OrderFilter filter) {
log.debug("Using experimental search for user: {}", userId);
// Implement experimental search logic
return Collections.emptyList();
}
private List<Order> traditionalOrderSearch(String userId, OrderFilter filter) {
log.debug("Using traditional search for user: {}", userId);
// Implement traditional search logic
return Collections.emptyList();
}
}

Monitoring and Health Checks

1. Configuration Health Indicator
@Component
public class DynamicConfigHealthIndicator implements HealthIndicator {
private final DynamicConfigManager configManager;
public DynamicConfigHealthIndicator(DynamicConfigManager configManager) {
this.configManager = configManager;
}
@Override
public Health health() {
try {
ConfigManagerMetrics metrics = configManager.getMetrics();
long secondsSinceUpdate = metrics.getSecondsSinceLastUpdate();
Health.Builder builder = secondsSinceUpdate < 300 ? Health.up() : Health.down();
return builder
.withDetail("configCount", metrics.getConfigCount())
.withDetail("featureFlagCount", metrics.getFeatureFlagCount())
.withDetail("lastUpdate", metrics.getLastUpdate())
.withDetail("secondsSinceUpdate", secondsSinceUpdate)
.build();
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}
2. Configuration Metrics
@Component
@Slf4j
public class ConfigMetricsCollector {
private final DynamicConfigManager configManager;
private final MeterRegistry meterRegistry;
public ConfigMetricsCollector(DynamicConfigManager configManager,
MeterRegistry meterRegistry) {
this.configManager = configManager;
this.meterRegistry = meterRegistry;
setupMetrics();
}
private void setupMetrics() {
// Gauge for config count
Gauge.builder("config.count", configManager, 
manager -> manager.getMetrics().getConfigCount())
.description("Number of dynamic configurations")
.register(meterRegistry);
// Gauge for feature flag count
Gauge.builder("featureflag.count", configManager,
manager -> manager.getMetrics().getFeatureFlagCount())
.description("Number of feature flags")
.register(meterRegistry);
// Timer for config access
Timer.builder("config.access.time")
.description("Time taken to access configurations")
.register(meterRegistry);
}
@Timed(value = "config.access.time", description = "Time to access configuration")
public Object getConfigWithTiming(String key) {
return configManager.getAllConfigs().get(key);
}
}

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class)
class DynamicConfigManagerTest {
@Mock
private DynamicConfigRepository configRepository;
@Mock
private FeatureFlagRepository featureFlagRepository;
@Mock
private ApplicationEventPublisher eventPublisher;
@InjectMocks
private DynamicConfigManager configManager;
@Test
void testGetConfig() {
// Given
DynamicConfig config = new DynamicConfig();
config.setKey("test.key");
config.setValue("test-value");
config.setType(ConfigType.STRING);
when(configRepository.findById("test.key")).thenReturn(Optional.of(config));
// When
String value = configManager.getConfig("test.key");
// Then
assertEquals("test-value", value);
}
@Test
void testSetConfig() {
// Given
DynamicConfig config = new DynamicConfig();
config.setKey("new.key");
config.setValue("new-value");
when(configRepository.save(any())).thenReturn(config);
// When
configManager.setConfig("new.key", "new-value", ConfigType.STRING);
// Then
verify(configRepository).save(any());
verify(eventPublisher).publishEvent(any(ConfigChangedEvent.class));
}
}
@SpringBootTest
class FeatureToggleIntegrationTest {
@Autowired
private FeatureToggleService featureToggleService;
@Autowired
private DynamicConfigManager configManager;
@Test
void testUserSpecificFeatureFlag() {
// Given
FeatureFlag flag = new FeatureFlag();
flag.setName("test-feature");
flag.setEnabled(true);
flag.setRolloutPercentage(50);
configManager.setFeatureFlag(flag);
// When & Then
assertTrue(featureToggleService.isExperimentalSearchEnabled("user-25")); // In rollout
assertFalse(featureToggleService.isExperimentalSearchEnabled("user-75")); // Not in rollout
}
}

Best Practices

  1. Fail-Safe Defaults: Always provide sensible defaults for configurations
  2. Validation: Validate configuration values before applying them
  3. Monitoring: Monitor configuration changes and their impact
  4. Security: Secure configuration endpoints and sensitive data
  5. Testing: Test configuration changes in isolation
  6. Documentation: Document available configurations and their effects
// Example of configuration validation
@Component
@Slf4j
public class ConfigValidator {
private final DynamicConfigManager configManager;
public ConfigValidator(DynamicConfigManager configManager) {
this.configManager = configManager;
}
@EventListener
public void validateConfigChange(ConfigChangedEvent event) {
String key = event.getConfigKey();
Object value = event.getNewValue();
switch (key) {
case "app.rate.limit.requests":
validateRateLimit((Integer) value);
break;
case "app.pricing.multiplier":
validatePricingMultiplier((Double) value);
break;
case "app.database.max-pool-size":
validateDatabasePoolSize((Integer) value);
break;
}
}
private void validateRateLimit(int requests) {
if (requests < 1 || requests > 10000) {
throw new ValidationException("Rate limit must be between 1 and 10000");
}
}
private void validatePricingMultiplier(double multiplier) {
if (multiplier < 0.1 || multiplier > 10.0) {
throw new ValidationException("Pricing multiplier must be between 0.1 and 10.0");
}
}
private void validateDatabasePoolSize(int poolSize) {
if (poolSize < 1 || poolSize > 100) {
throw new ValidationException("Database pool size must be between 1 and 100");
}
}
}

Conclusion

Dynamic configuration updates provide:

  • Real-time configuration changes without application restarts
  • Feature flag management for A/B testing and gradual rollouts
  • Runtime behavior modification for emergency controls and optimizations
  • Centralized configuration management with audit trails

This comprehensive implementation enables robust dynamic configuration management for Java applications, supporting everything from simple feature toggles to complex runtime behavior modifications. The solution integrates with Spring ecosystem components while providing a clean API for configuration management and monitoring.

Java Logistics, Shipping Integration & Enterprise Inventory Automation (Tracking, ERP, RFID & Billing Systems)

https://macronepal.com/blog/aftership-tracking-in-java-enterprise-package-visibility/
Explains how to integrate AfterShip tracking services into Java applications to provide real-time shipment visibility, delivery status updates, and centralized tracking across multiple courier services.

https://macronepal.com/blog/shipping-integration-using-fedex-api-with-java-for-logistics-automation/
Explains how to integrate the FedEx API into Java systems to automate shipping tasks such as creating shipments, calculating delivery costs, generating shipping labels, and tracking packages.

https://macronepal.com/blog/shipping-and-logistics-integrating-ups-apis-with-java-applications/
Explains UPS API integration in Java to enable automated shipping operations including rate calculation, shipment scheduling, tracking, and delivery confirmation management.

https://macronepal.com/blog/generating-and-reading-qr-codes-for-products-in-java/
Explains how Java applications generate and read QR codes for product identification, tracking, and authentication, supporting faster inventory handling and product verification processes.

https://macronepal.com/blog/designing-a-robust-pick-and-pack-workflow-in-java/
Explains how to design an efficient pick-and-pack workflow in Java warehouse systems, covering order processing, item selection, packaging steps, and logistics preparation to improve fulfillment efficiency.

https://macronepal.com/blog/rfid-inventory-management-system-in-java-a-complete-guide/
Explains how RFID technology integrates with Java applications to automate inventory tracking, reduce manual errors, and enable real-time stock monitoring in warehouses and retail environments.

https://macronepal.com/blog/erp-integration-with-odoo-in-java/
Explains how Java applications connect with Odoo ERP systems to synchronize inventory, orders, customer records, and financial data across enterprise systems.

https://macronepal.com/blog/automated-invoice-generation-creating-professional-excel-invoices-with-apache-poi-in-java/
Explains how to automatically generate professional Excel invoices in Java using Apache POI, enabling structured billing documents and automated financial record creation.

https://macronepal.com/blog/enterprise-financial-integration-using-quickbooks-api-in-java-applications/
Explains QuickBooks API integration in Java to automate financial workflows such as invoice management, payment tracking, accounting synchronization, and financial reporting.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper