Dynamic Configuration: Mastering @RefreshScope for Live Configuration Updates in Java

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

  1. Use Judiciously: Only apply @RefreshScope to components that truly need dynamic updates
  2. Validate Changes: Implement validation logic to ensure new configurations are valid
  3. Monitor Impact: Track configuration changes and their effects on application behavior
  4. Implement Rollback: Have mechanisms to revert problematic configuration changes
  5. Use Feature Flags: Leverage configuration refresh for feature toggling
  6. Secure Endpoints: Protect refresh endpoints in production environments
  7. Test Thoroughly: Ensure configuration changes don't break application functionality
  8. 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.

Leave a Reply

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


Macro Nepal Helper