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.