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
- Fail-Safe Defaults: Always provide sensible defaults for configurations
- Validation: Validate configuration values before applying them
- Monitoring: Monitor configuration changes and their impact
- Security: Secure configuration endpoints and sensitive data
- Testing: Test configuration changes in isolation
- 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.