Article
In modern microservices architectures, the ability to update application configuration without restarting services is crucial for maintaining high availability and rapid iteration. Spring Cloud's @RefreshScope provides a powerful mechanism to reload configuration properties at runtime, enabling dynamic updates from various configuration sources.
In this guide, we'll explore how to implement and leverage @RefreshScope in Java applications for seamless configuration management.
Why @RefreshScope Matters
- Zero Downtime Updates: Change configuration without service restarts
- Runtime Flexibility: Adapt to changing environments dynamically
- Centralized Configuration: Integrate with Spring Cloud Config, Consul, etc.
- Feature Flagging: Enable/disable features without redeployment
- Emergency Controls: Quickly modify behavior in production incidents
Part 1: Fundamentals and Setup
1.1 Dependencies
<!-- pom.xml --> <properties> <spring-boot.version>3.1.0</spring-boot.version> <spring-cloud.version>2022.0.3</spring-cloud.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> <!-- Spring Cloud Context (for @RefreshScope) --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-context</artifactId> <version>4.0.2</version> </dependency> <!-- Spring Cloud Config Client (optional) --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> <version>4.0.2</version> </dependency> <!-- Observability --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> <version>1.11.5</version> </dependency> </dependencies> 1.2 Project Structure
config-refresh-app/ ├── src/ │ ├── main/java/com/example/config/ │ │ ├── properties/ │ │ ├── refresh/ │ │ └── health/ │ └── resources/ │ └── application.yaml ├── config/ │ ├── application-dev.yaml │ └── application-prod.yaml └── scripts/ └── refresh-config.sh
Part 2: Basic @RefreshScope Implementation
2.1 Application Configuration
# src/main/resources/application.yaml spring: application: name: config-refresh-app config: import: optional:configserver:http://localhost:8888 # Optional Config Server app: config: refresh: enabled: true timeout-ms: 5000 features: new-payment-service: false dark-mode: true experimental-api: false service: name: user-service version: 1.0.0 endpoints: timeout: 5000 retry-attempts: 3 circuit-breaker: enabled: true failure-threshold: 50 database: pool: max-size: 20 min-size: 5 timeout: 30000 connection: max-lifetime: 1800000 cache: redis: ttl: 3600 max-entries: 10000 local: enabled: true size: 1000 management: endpoints: web: exposure: include: health,info,metrics,refresh,configprops,env enabled-by-default: true endpoint: refresh: enabled: true health: show-details: always show-components: always info: env: enabled: true logging: level: com.example.config: DEBUG org.springframework.cloud: INFO
2.2 Refresh-Scoped Configuration Properties
// File: src/main/java/com/example/config/properties/AppConfigProperties.java package com.example.config.properties; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Component @RefreshScope @ConfigurationProperties(prefix = "app") @Validated public class AppConfigProperties { private ServiceConfig service; private DatabaseConfig database; private CacheConfig cache; private FeatureFlags features; private RefreshConfig refresh; // Getters and Setters public ServiceConfig getService() { return service; } public void setService(ServiceConfig service) { this.service = service; } public DatabaseConfig getDatabase() { return database; } public void setDatabase(DatabaseConfig database) { this.database = database; } public CacheConfig getCache() { return cache; } public void setCache(CacheConfig cache) { this.cache = cache; } public FeatureFlags getFeatures() { return features; } public void setFeatures(FeatureFlags features) { this.features = features; } public RefreshConfig getRefresh() { return refresh; } public void setRefresh(RefreshConfig refresh) { this.refresh = refresh; } // Nested Configuration Classes @Validated public static class ServiceConfig { @NotBlank private String name; @NotBlank private String version; private Endpoints endpoints = new Endpoints(); // Getters and Setters public String getName() { return name; } public void setName(String name) { this.name = name; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public Endpoints getEndpoints() { return endpoints; } public void setEndpoints(Endpoints endpoints) { this.endpoints = endpoints; } @Validated public static class Endpoints { @Min(1000) @Max(30000) private int timeout = 5000; @Min(1) @Max(10) private int retryAttempts = 3; private CircuitBreaker circuitBreaker = new CircuitBreaker(); // Getters and Setters public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } public int getRetryAttempts() { return retryAttempts; } public void setRetryAttempts(int retryAttempts) { this.retryAttempts = retryAttempts; } public CircuitBreaker getCircuitBreaker() { return circuitBreaker; } public void setCircuitBreaker(CircuitBreaker circuitBreaker) { this.circuitBreaker = circuitBreaker; } public static class CircuitBreaker { private boolean enabled = true; @Min(1) @Max(100) private int failureThreshold = 50; @Min(1000) @Max(60000) private int timeout = 10000; // Getters and Setters public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public int getFailureThreshold() { return failureThreshold; } public void setFailureThreshold(int failureThreshold) { this.failureThreshold = failureThreshold; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } } } } @Validated public static class DatabaseConfig { private PoolConfig pool = new PoolConfig(); private ConnectionConfig connection = new ConnectionConfig(); // Getters and Setters public PoolConfig getPool() { return pool; } public void setPool(PoolConfig pool) { this.pool = pool; } public ConnectionConfig getConnection() { return connection; } public void setConnection(ConnectionConfig connection) { this.connection = connection; } public static class PoolConfig { @Min(1) @Max(100) private int maxSize = 20; @Min(1) @Max(10) private int minSize = 5; @Min(1000) @Max(60000) private int timeout = 30000; // Getters and Setters public int getMaxSize() { return maxSize; } public void setMaxSize(int maxSize) { this.maxSize = maxSize; } public int getMinSize() { return minSize; } public void setMinSize(int minSize) { this.minSize = minSize; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } } public static class ConnectionConfig { @Min(60000) @Max(3600000) private int maxLifetime = 1800000; // Getters and Setters public int getMaxLifetime() { return maxLifetime; } public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; } } } public static class CacheConfig { private RedisConfig redis = new RedisConfig(); private LocalConfig local = new LocalConfig(); // Getters and Setters public RedisConfig getRedis() { return redis; } public void setRedis(RedisConfig redis) { this.redis = redis; } public LocalConfig getLocal() { return local; } public void setLocal(LocalConfig local) { this.local = local; } public static class RedisConfig { private int ttl = 3600; private int maxEntries = 10000; // Getters and Setters public int getTtl() { return ttl; } public void setTtl(int ttl) { this.ttl = ttl; } public int getMaxEntries() { return maxEntries; } public void setMaxEntries(int maxEntries) { this.maxEntries = maxEntries; } } public static class LocalConfig { private boolean enabled = true; private int size = 1000; // Getters and Setters public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public int getSize() { return size; } public void setSize(int size) { this.size = size; } } } public static class FeatureFlags { private boolean newPaymentService = false; private boolean darkMode = true; private boolean experimentalApi = false; private Map<String, Boolean> customFlags = new HashMap<>(); // Getters and Setters public boolean isNewPaymentService() { return newPaymentService; } public void setNewPaymentService(boolean newPaymentService) { this.newPaymentService = newPaymentService; } public boolean isDarkMode() { return darkMode; } public void setDarkMode(boolean darkMode) { this.darkMode = darkMode; } public boolean isExperimentalApi() { return experimentalApi; } public void setExperimentalApi(boolean experimentalApi) { this.experimentalApi = experimentalApi; } public Map<String, Boolean> getCustomFlags() { return customFlags; } public void setCustomFlags(Map<String, Boolean> customFlags) { this.customFlags = customFlags; } } public static class RefreshConfig { private boolean enabled = true; private long timeoutMs = 5000; private List<String> monitoredProperties = new ArrayList<>(); // Getters and Setters public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public long getTimeoutMs() { return timeoutMs; } public void setTimeoutMs(long timeoutMs) { this.timeoutMs = timeoutMs; } public List<String> getMonitoredProperties() { return monitoredProperties; } public void setMonitoredProperties(List<String> monitoredProperties) { this.monitoredProperties = monitoredProperties; } } } Part 3: Refresh-Scoped Services and Components
3.1 Refresh-Scoped Service
// File: src/main/java/com/example/config/refresh/RefreshableService.java package com.example.config.refresh; import com.example.config.properties.AppConfigProperties; import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Service; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @Service @RefreshScope public class RefreshableService { private static final Logger logger = LoggerFactory.getLogger(RefreshableService.class); private final AppConfigProperties appConfig; private final AtomicInteger refreshCount = new AtomicInteger(0); private final AtomicLong lastRefreshTime = new AtomicLong(System.currentTimeMillis()); // Runtime state that gets reset on refresh private volatile String currentConfigHash; private volatile boolean configurationValid = true; public RefreshableService(AppConfigProperties appConfig) { this.appConfig = appConfig; } @PostConstruct public void initialize() { computeConfigHash(); logger.info("RefreshableService initialized with config: {}", appConfig.getService().getName()); logger.info("Feature flags - New Payment: {}, Dark Mode: {}", appConfig.getFeatures().isNewPaymentService(), appConfig.getFeatures().isDarkMode()); } public ServiceConfig getCurrentConfig() { return new ServiceConfig( appConfig.getService().getName(), appConfig.getService().getVersion(), appConfig.getService().getEndpoints().getTimeout(), appConfig.getService().getEndpoints().getRetryAttempts(), appConfig.getFeatures().isNewPaymentService(), appConfig.getFeatures().isDarkMode(), refreshCount.get(), lastRefreshTime.get() ); } public FeatureStatus getFeatureStatus(String featureName) { boolean enabled = switch (featureName.toLowerCase()) { case "newpaymentservice" -> appConfig.getFeatures().isNewPaymentService(); case "darkmode" -> appConfig.getFeatures().isDarkMode(); case "experimentalapi" -> appConfig.getFeatures().isExperimentalApi(); default -> appConfig.getFeatures().getCustomFlags().getOrDefault(featureName, false); }; return new FeatureStatus(featureName, enabled, System.currentTimeMillis()); } public DatabaseConfig getDatabaseConfig() { return new DatabaseConfig( appConfig.getDatabase().getPool().getMaxSize(), appConfig.getDatabase().getPool().getMinSize(), appConfig.getDatabase().getPool().getTimeout(), appConfig.getDatabase().getConnection().getMaxLifetime() ); } public CacheConfig getCacheConfig() { return new CacheConfig( appConfig.getCache().getRedis().getTtl(), appConfig.getCache().getRedis().getMaxEntries(), appConfig.getCache().getLocal().isEnabled(), appConfig.getCache().getLocal().getSize() ); } public void onRefresh() { refreshCount.incrementAndGet(); lastRefreshTime.set(System.currentTimeMillis()); // Recompute configuration hash computeConfigHash(); // Validate new configuration validateConfiguration(); logger.info("Configuration refreshed (count: {}). New config hash: {}", refreshCount.get(), currentConfigHash); // Perform any necessary reinitialization reinitializeComponents(); } private void computeConfigHash() { String configString = String.format("%s-%s-%b-%b", appConfig.getService().getName(), appConfig.getService().getVersion(), appConfig.getFeatures().isNewPaymentService(), appConfig.getFeatures().isDarkMode()); this.currentConfigHash = Integer.toHexString(configString.hashCode()); } private void validateConfiguration() { try { // Validate critical configuration values if (appConfig.getService().getEndpoints().getTimeout() <= 0) { throw new IllegalArgumentException("Timeout must be positive"); } if (appConfig.getDatabase().getPool().getMaxSize() < appConfig.getDatabase().getPool().getMinSize()) { throw new IllegalArgumentException("Max pool size cannot be less than min pool size"); } configurationValid = true; logger.debug("Configuration validation passed"); } catch (Exception e) { configurationValid = false; logger.error("Configuration validation failed: {}", e.getMessage()); // In production, you might want to revert to previous configuration } } private void reinitializeComponents() { if (!configurationValid) { logger.warn("Skipping component reinitialization due to invalid configuration"); return; } try { // Reinitialize components that depend on configuration logger.info("Reinitializing components with new configuration"); // Example: Reconfigure connection pools, caches, etc. reconfigureDatabasePool(); reconfigureCache(); } catch (Exception e) { logger.error("Error reinitializing components: {}", e.getMessage()); } } private void reconfigureDatabasePool() { // In a real application, this would reconfigure your database connection pool logger.info("Reconfiguring database pool - Max: {}, Min: {}, Timeout: {}", appConfig.getDatabase().getPool().getMaxSize(), appConfig.getDatabase().getPool().getMinSize(), appConfig.getDatabase().getPool().getTimeout()); } private void reconfigureCache() { // In a real application, this would reconfigure your cache settings logger.info("Reconfiguring cache - Redis TTL: {}, Local enabled: {}", appConfig.getCache().getRedis().getTtl(), appConfig.getCache().getLocal().isEnabled()); } public RefreshStats getRefreshStats() { return new RefreshStats( refreshCount.get(), lastRefreshTime.get(), currentConfigHash, configurationValid ); } // Data classes public record ServiceConfig( String serviceName, String version, int timeout, int retryAttempts, boolean newPaymentEnabled, boolean darkModeEnabled, int refreshCount, long lastRefreshTime ) {} public record FeatureStatus(String featureName, boolean enabled, long checkTime) {} public record DatabaseConfig(int maxPoolSize, int minPoolSize, int timeout, int maxLifetime) {} public record CacheConfig(int redisTtl, int redisMaxEntries, boolean localEnabled, int localSize) {} public record RefreshStats(int totalRefreshes, long lastRefreshTime, String configHash, boolean configValid) {} } 3.2 Configuration Change Listener
// File: src/main/java/com/example/config/refresh/ConfigChangeListener.java package com.example.config.refresh; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; @Component public class ConfigChangeListener { private static final Logger logger = LoggerFactory.getLogger(ConfigChangeListener.class); private final ConcurrentMap<String, AtomicLong> propertyChangeCount = new ConcurrentHashMap<>(); private final AtomicLong totalRefreshEvents = new AtomicLong(0); private final RefreshableService refreshableService; public ConfigChangeListener(RefreshableService refreshableService) { this.refreshableService = refreshableService; } @EventListener public void onRefreshScopeRefreshed(RefreshScopeRefreshedEvent event) { totalRefreshEvents.incrementAndGet(); logger.info("RefreshScope refreshed event received. Source: {}", event.getSource()); // Notify the refreshable service refreshableService.onRefresh(); // Log refresh statistics logRefreshStatistics(); } @EventListener public void onEnvironmentChange(org.springframework.cloud.context.environment.EnvironmentChangeEvent event) { logger.info("Environment changed. Keys: {}", event.getKeys()); // Track which properties are changing for (String key : event.getKeys()) { propertyChangeCount.computeIfAbsent(key, k -> new AtomicLong(0)).incrementAndGet(); } logger.debug("Property change counts updated"); } public void recordPropertyAccess(String propertyName) { propertyChangeCount.putIfAbsent(propertyName, new AtomicLong(0)); } public ConfigChangeStats getConfigChangeStats() { return new ConfigChangeStats( totalRefreshEvents.get(), System.currentTimeMillis(), new ConcurrentHashMap<>(propertyChangeCount) ); } private void logRefreshStatistics() { logger.info("Configuration refresh statistics - Total refreshes: {}, Tracked properties: {}", totalRefreshEvents.get(), propertyChangeCount.size()); } public record ConfigChangeStats( long totalRefreshes, long timestamp, ConcurrentMap<String, AtomicLong> propertyChangeCounts ) {} } Part 4: Controllers and Endpoints
4.1 Configuration Controller
// File: src/main/java/com/example/config/refresh/ConfigController.java package com.example.config.refresh; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api/v1/config") public class ConfigController { private final RefreshableService refreshableService; private final ConfigChangeListener configChangeListener; public ConfigController(RefreshableService refreshableService, ConfigChangeListener configChangeListener) { this.refreshableService = refreshableService; this.configChangeListener = configChangeListener; } @GetMapping("/current") public ResponseEntity<RefreshableService.ServiceConfig> getCurrentConfig() { return ResponseEntity.ok(refreshableService.getCurrentConfig()); } @GetMapping("/features/{featureName}") public ResponseEntity<RefreshableService.FeatureStatus> getFeatureStatus( @PathVariable String featureName) { return ResponseEntity.ok(refreshableService.getFeatureStatus(featureName)); } @GetMapping("/database") public ResponseEntity<RefreshableService.DatabaseConfig> getDatabaseConfig() { return ResponseEntity.ok(refreshableService.getDatabaseConfig()); } @GetMapping("/cache") public ResponseEntity<RefreshableService.CacheConfig> getCacheConfig() { return ResponseEntity.ok(refreshableService.getCacheConfig()); } @GetMapping("/refresh/stats") public ResponseEntity<RefreshableService.RefreshStats> getRefreshStats() { return ResponseEntity.ok(refreshableService.getRefreshStats()); } @GetMapping("/change/stats") public ResponseEntity<ConfigChangeListener.ConfigChangeStats> getChangeStats() { return ResponseEntity.ok(configChangeListener.getConfigChangeStats()); } @PostMapping("/features/{featureName}/track") public ResponseEntity<Map<String, String>> trackFeature(@PathVariable String featureName) { configChangeListener.recordPropertyAccess("app.features." + featureName); return ResponseEntity.ok(Map.of( "status", "success", "message", "Now tracking feature: " + featureName )); } @GetMapping("/health") public ResponseEntity<Map<String, Object>> health() { RefreshableService.RefreshStats stats = refreshableService.getRefreshStats(); return ResponseEntity.ok(Map.of( "status", "healthy", "service", "config-refresh-app", "refreshEnabled", true, "totalRefreshes", stats.totalRefreshes(), "configValid", stats.configValid(), "lastRefresh", stats.lastRefreshTime() )); } } 4.2 Custom Refresh Endpoint
// File: src/main/java/com/example/config/refresh/CustomRefreshEndpoint.java package com.example.config.refresh; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; @Component @Endpoint(id = "customrefresh") public class CustomRefreshEndpoint { private final ContextRefresher contextRefresher; private final RefreshableService refreshableService; private final AtomicLong refreshRequests = new AtomicLong(0); public CustomRefreshEndpoint(ContextRefresher contextRefresher, RefreshableService refreshableService) { this.contextRefresher = contextRefresher; this.refreshableService = refreshableService; } @WriteOperation public Map<String, Object> refresh() { long requestId = refreshRequests.incrementAndGet(); try { Map<String, Object> result = new HashMap<>(); result.put("requestId", requestId); result.put("timestamp", System.currentTimeMillis()); result.put("status", "refresh_initiated"); // Perform the refresh var refreshResult = contextRefresher.refresh(); result.put("refreshedProperties", refreshResult); result.put("refreshCount", refreshableService.getRefreshStats().totalRefreshes()); result.put("message", "Configuration refresh completed successfully"); return result; } catch (Exception e) { Map<String, Object> errorResult = new HashMap<>(); errorResult.put("requestId", requestId); errorResult.put("timestamp", System.currentTimeMillis()); errorResult.put("status", "error"); errorResult.put("error", e.getMessage()); return errorResult; } } @WriteOperation public Map<String, Object> refreshWithValidation(boolean validateOnly) { long requestId = refreshRequests.incrementAndGet(); try { Map<String, Object> result = new HashMap<>(); result.put("requestId", requestId); result.put("timestamp", System.currentTimeMillis()); if (validateOnly) { // Simulate validation without actual refresh boolean configValid = refreshableService.getRefreshStats().configValid(); result.put("status", "validation_completed"); result.put("configValid", configValid); result.put("message", "Configuration validation completed"); } else { // Perform actual refresh var refreshResult = contextRefresher.refresh(); result.put("status", "refresh_completed"); result.put("refreshedProperties", refreshResult); result.put("configValid", refreshableService.getRefreshStats().configValid()); result.put("message", "Configuration refresh with validation completed"); } return result; } catch (Exception e) { Map<String, Object> errorResult = new HashMap<>(); errorResult.put("requestId", requestId); errorResult.put("timestamp", System.currentTimeMillis()); errorResult.put("status", "error"); errorResult.put("error", e.getMessage()); return errorResult; } } } Part 5: Advanced Refresh Patterns
5.1 Conditional Refresh Configuration
// File: src/main/java/com/example/config/refresh/ConditionalRefreshService.java package com.example.config.refresh; import com.example.config.properties.AppConfigProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @Service @RefreshScope public class ConditionalRefreshService { private static final Logger logger = LoggerFactory.getLogger(ConditionalRefreshService.class); private final AppConfigProperties appConfig; private final ContextRefresher contextRefresher; private final ConcurrentMap<String, Long> lastRefreshAttempts = new ConcurrentHashMap<>(); public ConditionalRefreshService(AppConfigProperties appConfig, ContextRefresher contextRefresher) { this.appConfig = appConfig; this.contextRefresher = contextRefresher; } public boolean shouldRefreshBasedOnConditions() { if (!appConfig.getRefresh().isEnabled()) { logger.debug("Refresh is disabled in configuration"); return false; } // Check if we're in a maintenance window or have other business logic if (isInMaintenanceWindow()) { logger.debug("In maintenance window, skipping refresh"); return false; } // Check rate limiting if (isRateLimited("conditional_refresh")) { logger.debug("Rate limited, skipping refresh"); return false; } return true; } @Scheduled(fixedRateString = "${app.config.refresh.check-interval:300000}") // 5 minutes public void scheduledConfigCheck() { if (shouldRefreshBasedOnConditions()) { logger.info("Scheduled config check passed conditions, initiating refresh"); performConditionalRefresh(); } } public RefreshResult performConditionalRefresh() { try { if (!shouldRefreshBasedOnConditions()) { return new RefreshResult(false, "Refresh conditions not met", null); } logger.info("Performing conditional configuration refresh"); var refreshedProperties = contextRefresher.refresh(); recordRefreshAttempt("conditional_refresh", true); return new RefreshResult(true, "Refresh completed successfully", refreshedProperties); } catch (Exception e) { logger.error("Conditional refresh failed: {}", e.getMessage()); recordRefreshAttempt("conditional_refresh", false); return new RefreshResult(false, "Refresh failed: " + e.getMessage(), null); } } private boolean isInMaintenanceWindow() { // Implement your maintenance window logic // For example, check current time against configured maintenance windows return false; } private boolean isRateLimited(String operation) { Long lastAttempt = lastRefreshAttempts.get(operation); if (lastAttempt == null) { return false; } long timeSinceLastAttempt = System.currentTimeMillis() - lastAttempt; long minInterval = appConfig.getRefresh().getTimeoutMs(); return timeSinceLastAttempt < minInterval; } private void recordRefreshAttempt(String operation, boolean success) { lastRefreshAttempts.put(operation, System.currentTimeMillis()); logger.debug("Recorded {} refresh attempt. Success: {}", operation, success); } public record RefreshResult(boolean success, String message, Object refreshedProperties) {} } 5.2 Configuration Versioning and Rollback
// File: src/main/java/com/example/config/refresh/ConfigVersioningService.java package com.example.config.refresh; import com.example.config.properties.AppConfigProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Service; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @Service @RefreshScope public class ConfigVersioningService { private static final Logger logger = LoggerFactory.getLogger(ConfigVersioningService.class); private final AppConfigProperties appConfig; private final List<ConfigVersion> configHistory = new CopyOnWriteArrayList<>(); private final Map<String, ConfigVersion> versionSnapshots = new ConcurrentHashMap<>(); private final int maxHistorySize = 10; public ConfigVersioningService(AppConfigProperties appConfig) { this.appConfig = appConfig; // Record initial configuration recordConfigVersion("initial", "Initial configuration"); } public ConfigVersion recordConfigVersion(String versionId, String description) { ConfigVersion version = new ConfigVersion( versionId, System.currentTimeMillis(), description, snapshotCurrentConfig() ); configHistory.add(version); versionSnapshots.put(versionId, version); // Maintain history size limit if (configHistory.size() > maxHistorySize) { ConfigVersion removed = configHistory.remove(0); versionSnapshots.remove(removed.versionId()); logger.debug("Removed old config version from history: {}", removed.versionId()); } logger.info("Recorded configuration version: {} - {}", versionId, description); return version; } public ConfigVersion getCurrentVersion() { if (configHistory.isEmpty()) { return null; } return configHistory.get(configHistory.size() - 1); } public List<ConfigVersion> getConfigHistory() { return new ArrayList<>(configHistory); } public Optional<ConfigVersion> getVersion(String versionId) { return Optional.ofNullable(versionSnapshots.get(versionId)); } public ConfigVersion createSnapshot(String description) { String versionId = "snapshot-" + System.currentTimeMillis(); return recordConfigVersion(versionId, description); } public ConfigComparison compareWithVersion(String versionId) { Optional<ConfigVersion> targetVersion = getVersion(versionId); if (targetVersion.isEmpty()) { throw new IllegalArgumentException("Version not found: " + versionId); } ConfigVersion currentVersion = getCurrentVersion(); Map<String, Object> differences = findConfigDifferences( targetVersion.get().configSnapshot(), currentVersion.configSnapshot() ); return new ConfigComparison( targetVersion.get(), currentVersion, differences ); } private Map<String, Object> snapshotCurrentConfig() { Map<String, Object> snapshot = new HashMap<>(); snapshot.put("service.name", appConfig.getService().getName()); snapshot.put("service.version", appConfig.getService().getVersion()); snapshot.put("service.endpoints.timeout", appConfig.getService().getEndpoints().getTimeout()); snapshot.put("features.newPaymentService", appConfig.getFeatures().isNewPaymentService()); snapshot.put("features.darkMode", appConfig.getFeatures().isDarkMode()); snapshot.put("database.pool.maxSize", appConfig.getDatabase().getPool().getMaxSize()); snapshot.put("cache.redis.ttl", appConfig.getCache().getRedis().getTtl()); return snapshot; } private Map<String, Object> findConfigDifferences(Map<String, Object> oldConfig, Map<String, Object> newConfig) { Map<String, Object> differences = new HashMap<>(); // Find keys in old config but not in new config oldConfig.forEach((key, oldValue) -> { Object newValue = newConfig.get(key); if (!Objects.equals(oldValue, newValue)) { differences.put(key, Map.of("old", oldValue, "new", newValue)); } }); // Find keys in new config but not in old config newConfig.forEach((key, newValue) -> { if (!oldConfig.containsKey(key)) { differences.put(key, Map.of("old", null, "new", newValue)); } }); return differences; } public record ConfigVersion( String versionId, long timestamp, String description, Map<String, Object> configSnapshot ) {} public record ConfigComparison( ConfigVersion baseVersion, ConfigVersion currentVersion, Map<String, Object> differences ) {} } Part 6: Testing Configuration Refresh
6.1 Test Configuration
// File: src/test/java/com/example/config/refresh/ConfigRefreshTest.java package com.example.config.refresh; import com.example.config.properties.AppConfigProperties; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.test.context.TestPropertySource; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @TestPropertySource(properties = { "app.service.name=test-service", "app.service.version=1.0.0", "app.features.new-payment-service=false", "app.features.dark-mode=true" }) class ConfigRefreshTest { @Autowired private RefreshableService refreshableService; @Autowired private AppConfigProperties appConfigProperties; @Test void testConfigurationPropertiesLoaded() { assertNotNull(appConfigProperties); assertEquals("test-service", appConfigProperties.getService().getName()); assertEquals("1.0.0", appConfigProperties.getService().getVersion()); assertFalse(appConfigProperties.getFeatures().isNewPaymentService()); assertTrue(appConfigProperties.getFeatures().isDarkMode()); } @Test void testRefreshableServiceInitialization() { var config = refreshableService.getCurrentConfig(); assertNotNull(config); assertEquals("test-service", config.serviceName()); assertEquals("1.0.0", config.version()); assertFalse(config.newPaymentEnabled()); assertTrue(config.darkModeEnabled()); } @Test void testFeatureStatus() { var featureStatus = refreshableService.getFeatureStatus("darkMode"); assertNotNull(featureStatus); assertEquals("darkMode", featureStatus.featureName()); assertTrue(featureStatus.enabled()); } @Test void testRefreshStats() { var stats = refreshableService.getRefreshStats(); assertNotNull(stats); assertTrue(stats.totalRefreshes() >= 0); assertNotNull(stats.configHash()); } } 6.2 Integration Test
// File: src/test/java/com/example/config/refresh/ConfigRefreshIntegrationTest.java package com.example.config.refresh; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.ResponseEntity; import org.springframework.test.context.TestPropertySource; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestPropertySource(properties = { "management.endpoints.web.exposure.include=*" }) class ConfigRefreshIntegrationTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Test void testConfigEndpoints() { String baseUrl = "http://localhost:" + port; // Test current config endpoint ResponseEntity<Map> configResponse = restTemplate.getForEntity( baseUrl + "/api/v1/config/current", Map.class); assertTrue(configResponse.getStatusCode().is2xxSuccessful()); assertNotNull(configResponse.getBody()); // Test health endpoint ResponseEntity<Map> healthResponse = restTemplate.getForEntity( baseUrl + "/api/v1/config/health", Map.class); assertTrue(healthResponse.getStatusCode().is2xxSuccessful()); assertEquals("healthy", healthResponse.getBody().get("status")); // Test refresh stats endpoint ResponseEntity<Map> statsResponse = restTemplate.getForEntity( baseUrl + "/api/v1/config/refresh/stats", Map.class); assertTrue(statsResponse.getStatusCode().is2xxSuccessful()); assertNotNull(statsResponse.getBody()); } } Best Practices for @RefreshScope
- Use Judiciously: Only apply
@RefreshScopeto components that truly need dynamic updates - Validate Changes: Implement validation logic to ensure new configurations are valid
- Monitor Impact: Track configuration changes and their effects on application behavior
- Implement Rollback: Have mechanisms to revert problematic configuration changes
- Use Feature Flags: Leverage configuration refresh for feature toggling
- Secure Endpoints: Protect refresh endpoints in production environments
- Test Thoroughly: Ensure configuration changes don't break application functionality
- Document Changes: Maintain audit trails of configuration modifications
Conclusion
Implementing @RefreshScope in Java applications provides powerful capabilities for dynamic configuration management. By following the patterns and best practices outlined in this guide, you can:
- Achieve zero-downtime configuration updates without service restarts
- Implement feature flagging for controlled feature rollouts
- Create self-healing applications that can adapt to configuration changes
- Maintain comprehensive audit trails of configuration modifications
- Build resilient systems with proper validation and rollback mechanisms
The combination of @RefreshScope with proper architecture patterns enables truly dynamic applications that can adapt to changing requirements while maintaining stability and observability.
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.