Dynamic Configuration Updates: Implementing ConfigMap Reloading in Java Applications

In Kubernetes, ConfigMaps are essential for externalizing configuration from your Java applications. But what happens when you need to update configuration without restarting your application? ConfigMap reloading enables dynamic configuration updates while your application continues running. Let's explore various strategies to implement ConfigMap reloading in Java applications.

Understanding ConfigMap Updates in Kubernetes

When a ConfigMap is updated in Kubernetes:

  • The update is eventually propagated to mounted volumes
  • Environment variables from ConfigMaps are NOT updated - they're injected only at pod startup
  • Volume-mounted ConfigMap files ARE updated - typically within 1-2 minutes

Strategy 1: File Watching with Spring Boot

Basic Spring Boot Configuration

// src/main/java/com/company/config/ReloadableConfig.java
@Configuration
@ConfigurationProperties(prefix = "app")
@Data
public class AppConfig {
private String featureFlag;
private int timeout;
private String apiUrl;
private List<String> allowedHosts;
}

File Watcher Implementation

// src/main/java/com/company/config/ConfigMapWatcher.java
@Component
@Slf4j
public class ConfigMapWatcher {
private final AppConfig appConfig;
private final ObjectMapper objectMapper;
private WatchService watchService;
private Path configPath;
public ConfigMapWatcher(AppConfig appConfig, 
@Value("${config.path:/etc/config}") String configPath) {
this.appConfig = appConfig;
this.objectMapper = new ObjectMapper();
this.configPath = Paths.get(configPath);
initializeWatcher();
}
@PostConstruct
public void initializeWatcher() {
try {
this.watchService = FileSystems.getDefault().newWatchService();
configPath.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
Thread watchThread = new Thread(this::watchForChanges);
watchThread.setDaemon(true);
watchThread.setName("config-watcher");
watchThread.start();
log.info("ConfigMap watcher initialized for path: {}", configPath);
} catch (IOException e) {
log.error("Failed to initialize config watcher", e);
}
}
private void watchForChanges() {
while (!Thread.currentThread().isInterrupted()) {
try {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
Path changedFile = (Path) event.context();
if (changedFile.toString().equals("application.properties") || 
changedFile.toString().equals("app-config.json")) {
log.info("Config file changed: {}", changedFile);
reloadConfiguration();
}
}
}
boolean valid = key.reset();
if (!valid) {
log.error("Watch key no longer valid");
break;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.info("Config watcher interrupted");
} catch (Exception e) {
log.error("Error in config watcher", e);
}
}
}
private void reloadConfiguration() {
try {
// Reload from mounted ConfigMap volume
Properties properties = new Properties();
Path propertiesFile = configPath.resolve("application.properties");
if (Files.exists(propertiesFile)) {
try (InputStream input = Files.newInputStream(propertiesFile)) {
properties.load(input);
}
// Update configuration beans
updateAppConfig(properties);
log.info("Configuration reloaded successfully");
}
} catch (Exception e) {
log.error("Failed to reload configuration", e);
}
}
private void updateAppConfig(Properties properties) {
// Update the configuration object
if (properties.containsKey("app.feature-flag")) {
appConfig.setFeatureFlag(properties.getProperty("app.feature-flag"));
}
if (properties.containsKey("app.timeout")) {
appConfig.setTimeout(Integer.parseInt(properties.getProperty("app.timeout")));
}
if (properties.containsKey("app.api-url")) {
appConfig.setApiUrl(properties.getProperty("app.api-url"));
}
log.info("Updated AppConfig - featureFlag: {}, timeout: {}", 
appConfig.getFeatureFlag(), appConfig.getTimeout());
}
}

Strategy 2: Spring Cloud Kubernetes Reload

Dependencies Setup

<!-- pom.xml -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-fabric8-all</artifactId>
<version>2.1.7</version>
</dependency>

Configuration

# application.yml
spring:
cloud:
kubernetes:
reload:
enabled: true
mode: event
strategy: refresh
config:
sources:
- name: ${spring.application.name}
namespace: default

Application Setup

// src/main/java/com/company/Application.java
@SpringBootApplication
@Configuration
@RefreshScope
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
@RefreshScope
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
// Config class that will be refreshed
@Component
@RefreshScope
@Data
public class DynamicConfig {
@Value("${app.feature.enabled:false}")
private boolean featureEnabled;
@Value("${app.cache.timeout:300}")
private int cacheTimeout;
@Value("${app.api.endpoint:http://localhost:8080}")
private String apiEndpoint;
}

Strategy 3: Kubernetes API Watch Implementation

Direct Kubernetes API Integration

// src/main/java/com/company/k8s/KubernetesConfigWatcher.java
@Component
@Slf4j
public class KubernetesConfigWatcher {
private final KubernetesClient kubernetesClient;
private final ApplicationEventPublisher eventPublisher;
private final String namespace;
private final String configMapName;
public KubernetesConfigWatcher(KubernetesClient kubernetesClient,
ApplicationEventPublisher eventPublisher,
@Value("${app.configmap.name:app-config}") String configMapName) {
this.kubernetesClient = kubernetesClient;
this.eventPublisher = eventPublisher;
this.configMapName = configMapName;
this.namespace = getCurrentNamespace();
}
@PostConstruct
public void startWatching() {
Thread watchThread = new Thread(this::watchConfigMap);
watchThread.setDaemon(true);
watchThread.setName("k8s-config-watcher");
watchThread.start();
}
private void watchConfigMap() {
try {
kubernetesClient.configMaps()
.inNamespace(namespace)
.withName(configMapName)
.watch(new Watcher<ConfigMap>() {
@Override
public void eventReceived(Action action, ConfigMap configMap) {
if (action == Action.MODIFIED) {
log.info("ConfigMap {} modified", configMapName);
eventPublisher.publishEvent(new ConfigMapUpdateEvent(this, configMap.getData()));
}
}
@Override
public void onClose(WatcherException cause) {
log.warn("ConfigMap watch closed, reconnecting...", cause);
// Implement reconnection logic
}
});
log.info("Started watching ConfigMap: {} in namespace: {}", configMapName, namespace);
} catch (Exception e) {
log.error("Failed to watch ConfigMap", e);
}
}
private String getCurrentNamespace() {
try {
return kubernetesClient.getNamespace();
} catch (Exception e) {
log.warn("Could not determine namespace, using 'default'", e);
return "default";
}
}
}
// Custom event for configuration updates
public class ConfigMapUpdateEvent extends ApplicationEvent {
private final Map<String, String> configData;
public ConfigMapUpdateEvent(Object source, Map<String, String> configData) {
super(source);
this.configData = configData;
}
public Map<String, String> getConfigData() {
return configData;
}
}
// Event listener to handle updates
@Component
@Slf4j
public class ConfigMapUpdateListener {
private final DynamicConfig dynamicConfig;
public ConfigMapUpdateListener(DynamicConfig dynamicConfig) {
this.dynamicConfig = dynamicConfig;
}
@EventListener
public void onConfigMapUpdate(ConfigMapUpdateEvent event) {
log.info("Processing ConfigMap update");
Map<String, String> newData = event.getConfigData();
// Update configuration based on new data
if (newData.containsKey("feature.enabled")) {
dynamicConfig.setFeatureEnabled(Boolean.parseBoolean(newData.get("feature.enabled")));
}
if (newData.containsKey("cache.timeout")) {
dynamicConfig.setCacheTimeout(Integer.parseInt(newData.get("cache.timeout")));
}
log.info("Dynamic configuration updated: featureEnabled={}, cacheTimeout={}",
dynamicConfig.isFeatureEnabled(), dynamicConfig.getCacheTimeout());
}
}

Strategy 4: Scheduled Configuration Reload

Periodic Config Checker

// src/main/java/com/company/config/ScheduledConfigReloader.java
@Component
@Slf4j
@EnableScheduling
public class ScheduledConfigReloader {
private final ConfigMapWatcher configMapWatcher;
private final File configFile;
private long lastModified;
public ScheduledConfigReloader(@Value("${config.file:/etc/config/app.properties}") String configFilePath) {
this.configMapWatcher = configMapWatcher;
this.configFile = new File(configFilePath);
this.lastModified = configFile.lastModified();
}
@Scheduled(fixedDelay = 30000) // Check every 30 seconds
public void checkForConfigChanges() {
if (!configFile.exists()) {
return;
}
long currentModified = configFile.lastModified();
if (currentModified > lastModified) {
log.info("Config file changed detected, reloading configuration...");
lastModified = currentModified;
configMapWatcher.reloadConfiguration();
}
}
}

Complete Kubernetes Deployment Example

ConfigMap Definition

# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: java-app-config
labels:
app: java-app
data:
application.properties: |
app.feature-flag=ENABLED
app.timeout=5000
app.api-url=https://api.company.com/v1
app.cache.enabled=true
app.retry.count=3
logback.xml: |
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>

Deployment with Volume Mounts

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-app
spec:
replicas: 2
selector:
matchLabels:
app: java-app
template:
metadata:
labels:
app: java-app
spec:
containers:
- name: java-app
image: my-registry/java-app:latest
ports:
- containerPort: 8080
volumeMounts:
- name: config-volume
mountPath: /etc/config
readOnly: true
- name: logging-config-volume
mountPath: /app/config
readOnly: true
env:
- name: CONFIG_PATH
value: "/etc/config"
- name: LOG_CONFIG_PATH
value: "/app/config"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: config-volume
configMap:
name: java-app-config
items:
- key: application.properties
path: application.properties
- name: logging-config-volume
configMap:
name: java-app-config
items:
- key: logback.xml
path: logback.xml

Advanced: Dynamic Log Level Configuration

// src/main/java/com/company/config/DynamicLoggingConfig.java
@Component
@Slf4j
public class DynamicLoggingConfig {
private final LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
@EventListener
public void onConfigMapUpdate(ConfigMapUpdateEvent event) {
Map<String, String> configData = event.getConfigData();
if (configData.containsKey("log.level.root")) {
String logLevel = configData.get("log.level.root");
setLogLevel("ROOT", logLevel);
log.info("Updated root log level to: {}", logLevel);
}
if (configData.containsKey("log.level.com.company")) {
String logLevel = configData.get("log.level.com.company");
setLogLevel("com.company", logLevel);
log.info("Updated package log level to: {}", logLevel);
}
}
private void setLogLevel(String loggerName, String level) {
Logger logger = loggerContext.getLogger(loggerName);
switch (level.toUpperCase()) {
case "TRACE":
logger.setLevel(Level.TRACE);
break;
case "DEBUG":
logger.setLevel(Level.DEBUG);
break;
case "INFO":
logger.setLevel(Level.INFO);
break;
case "WARN":
logger.setLevel(Level.WARN);
break;
case "ERROR":
logger.setLevel(Level.ERROR);
break;
default:
logger.setLevel(null); // Inherit from parent
}
}
}

Testing ConfigMap Reloading

Unit Test

// src/test/java/com/company/config/ConfigMapWatcherTest.java
@ExtendWith(MockitoExtension.class)
class ConfigMapWatcherTest {
@Mock
private AppConfig appConfig;
@TempDir
Path tempDir;
private ConfigMapWatcher configMapWatcher;
@Test
void testConfigReload() throws Exception {
// Create test config file
Path configFile = tempDir.resolve("application.properties");
Files.write(configFile, "app.feature-flag=TEST\napp.timeout=1000".getBytes());
configMapWatcher = new ConfigMapWatcher(appConfig, tempDir.toString());
// Update config file
Files.write(configFile, "app.feature-flag=PROD\napp.timeout=2000".getBytes());
// Allow time for file system events
Thread.sleep(100);
verify(appConfig, times(1)).setFeatureFlag("PROD");
verify(appConfig, times(1)).setTimeout(2000);
}
}

Best Practices for ConfigMap Reloading

  1. Use Readiness Probes: Update readiness status during configuration reload
  2. Implement Circuit Breakers: Prevent issues during configuration transitions
  3. Version Your ConfigMaps: Include version information in ConfigMap metadata
  4. Graceful Fallbacks: Maintain previous configuration if reload fails
  5. Monitoring: Track configuration reload events and failures
  6. Resource Limits: Set appropriate memory and CPU limits for watch operations
// Health indicator for configuration state
@Component
public class ConfigHealthIndicator implements HealthIndicator {
private final ConfigMapWatcher configWatcher;
private volatile boolean configValid = true;
@Override
public Health health() {
if (configValid) {
return Health.up().withDetail("config", "healthy").build();
} else {
return Health.down().withDetail("config", "reload failed").build();
}
}
}

Conclusion

ConfigMap reloading in Java applications provides powerful dynamic configuration capabilities:

  • Spring Cloud Kubernetes: Simplest approach for Spring Boot applications
  • File Watching: Universal approach that works with any Java application
  • Kubernetes API Watch: Most responsive but requires more setup
  • Scheduled Reloading: Simple and reliable for less frequent updates

Choose the strategy that best fits your application's requirements:

  • Use Spring Cloud Kubernetes for full-featured Spring Boot applications
  • Use file watching for non-Spring applications or when you need fine-grained control
  • Use Kubernetes API watch for real-time updates and complex configuration scenarios

By implementing ConfigMap reloading, your Java applications can adapt to configuration changes without downtime, improving operational flexibility and deployment velocity.

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