ConfigMap and Secret Reloading in Java: Complete Implementation Guide

Introduction

In Kubernetes, ConfigMaps and Secrets are used to manage configuration data and sensitive information. However, when these resources are updated, applications don't automatically reload the new configuration. This guide covers multiple strategies to implement ConfigMap and Secret reloading in Java applications running in Kubernetes.


Architecture Overview

[Kubernetes] → [ConfigMap/Secret] → [File System] → [File Watcher] → [Application] → [Configuration Reload]
↓              ↓                   ↓              ↓               ↓               ↓
kubectl       Volume Mount       Config Files    Watch Service   Spring Beans    Dynamic Updates
Operator      Environment Vars   Symlinks        Polling         Configuration   Bean Refresh
GitOps        External Secrets   Sidecar        Inotify         Properties      Cache Clear

Step 1: Project Dependencies

Maven Configuration

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>config-reload</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>3.2.0</spring-boot.version>
<kubernetes-client.version>6.8.1</kubernetes-client.version>
<micrometer.version>1.12.0</micrometer.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Configuration Processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring-boot.version}</version>
<optional>true</optional>
</dependency>
<!-- Kubernetes Client -->
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java</artifactId>
<version>${kubernetes-client.version}</version>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java-api-fluent</artifactId>
<version>${kubernetes-client.version}</version>
</dependency>
<!-- Metrics -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>${micrometer.version}</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>${micrometer.version}</version>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.15.0</version>
</dependency>
<!-- YAML -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.15.0</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.18.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>

Step 2: Kubernetes Manifests

ConfigMap and Secret Definitions

k8s/configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: default
labels:
app: java-app
data:
application.yaml: |
app:
name: "Config Reload Demo"
version: "1.0.0"
database:
url: "jdbc:postgresql://localhost:5432/mydb"
pool:
min-size: 2
max-size: 10
timeout: 30000
features:
dark-mode: true
experimental-features: false
rate-limiting:
enabled: true
requests-per-minute: 100
logging:
level:
com.example: INFO
org.springframework: WARN
feature-flags.yaml: |
features:
new-ui:
enabled: false
rollout-percentage: 10
payment-v2:
enabled: true
rollout-percentage: 50
search-v3:
enabled: false
rollout-percentage: 0

k8s/secret.yaml

apiVersion: v1
kind: Secret
metadata:
name: app-secrets
namespace: default
labels:
app: java-app
type: Opaque
data:
database-password: c3VwZXJzZWNyZXRwYXNzd29yZA==  # base64 encoded
api-key: YXBpLWtleS1zZWNyZXQ=
jwt-secret: anN0LXNlY3JldC1rZXk=

Deployment with Volume Mounts

k8s/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
name: java-app
namespace: default
labels:
app: java-app
spec:
replicas: 2
selector:
matchLabels:
app: java-app
template:
metadata:
labels:
app: java-app
annotations:
# Trigger redeploy when config changes
config.hash: "${CONFIG_HASH}"
secret.hash: "${SECRET_HASH}"
spec:
containers:
- name: java-app
image: myapp:latest
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "kubernetes"
- name: CONFIG_RELOAD_ENABLED
value: "true"
- name: CONFIG_RELOAD_INTERVAL
value: "30000"
# Mount ConfigMap and Secret as volumes
volumeMounts:
- name: config-volume
mountPath: /etc/app/config
readOnly: true
- name: secret-volume
mountPath: /etc/app/secrets
readOnly: true
- name: temp-volume
mountPath: /tmp
# Resource limits
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
# Liveness and readiness probes
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 15
periodSeconds: 5
timeoutSeconds: 3
# Security context
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
# Volumes from ConfigMap and Secret
volumes:
- name: config-volume
configMap:
name: app-config
items:
- key: application.yaml
path: application.yaml
- key: feature-flags.yaml
path: feature-flags.yaml
- name: secret-volume
secret:
secretName: app-secrets
items:
- key: database-password
path: database-password
- key: api-key
path: api-key
- key: jwt-secret
path: jwt-secret
- name: temp-volume
emptyDir: {}
# Security context
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000

Step 3: Configuration Models

Application Configuration

AppConfig.java

package com.example.configreload.model;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import java.util.Map;
@ConfigurationProperties(prefix = "app")
@Validated
public class AppConfig {
@NotBlank
private String name;
@NotBlank
private String version;
@NotNull
private DatabaseConfig database;
@NotNull
private FeaturesConfig features;
@NotNull
private LoggingConfig logging;
// 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 DatabaseConfig getDatabase() { return database; }
public void setDatabase(DatabaseConfig database) { this.database = database; }
public FeaturesConfig getFeatures() { return features; }
public void setFeatures(FeaturesConfig features) { this.features = features; }
public LoggingConfig getLogging() { return logging; }
public void setLogging(LoggingConfig logging) { this.logging = logging; }
public static class DatabaseConfig {
@NotBlank
private String url;
@Positive
private int minSize;
@Positive
private int maxSize;
@Positive
private long timeout;
// Getters and Setters
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public int getMinSize() { return minSize; }
public void setMinSize(int minSize) { this.minSize = minSize; }
public int getMaxSize() { return maxSize; }
public void setMaxSize(int maxSize) { this.maxSize = maxSize; }
public long getTimeout() { return timeout; }
public void setTimeout(long timeout) { this.timeout = timeout; }
}
public static class FeaturesConfig {
private boolean darkMode;
private boolean experimentalFeatures;
@NotNull
private RateLimitingConfig rateLimiting;
// Getters and Setters
public boolean isDarkMode() { return darkMode; }
public void setDarkMode(boolean darkMode) { this.darkMode = darkMode; }
public boolean isExperimentalFeatures() { return experimentalFeatures; }
public void setExperimentalFeatures(boolean experimentalFeatures) { this.experimentalFeatures = experimentalFeatures; }
public RateLimitingConfig getRateLimiting() { return rateLimiting; }
public void setRateLimiting(RateLimitingConfig rateLimiting) { this.rateLimiting = rateLimiting; }
public static class RateLimitingConfig {
private boolean enabled;
@Positive
private int requestsPerMinute;
// Getters and Setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public int getRequestsPerMinute() { return requestsPerMinute; }
public void setRequestsPerMinute(int requestsPerMinute) { this.requestsPerMinute = requestsPerMinute; }
}
}
public static class LoggingConfig {
@NotNull
private Map<String, String> level;
// Getters and Setters
public Map<String, String> getLevel() { return level; }
public void setLevel(Map<String, String> level) { this.level = level; }
}
}

Feature Flags Configuration

FeatureFlags.java

package com.example.configreload.model;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import java.util.Map;
@ConfigurationProperties(prefix = "features")
@Validated
public class FeatureFlags {
private Map<String, FeatureFlag> features;
// Getters and Setters
public Map<String, FeatureFlag> getFeatures() { return features; }
public void setFeatures(Map<String, FeatureFlag> features) { this.features = features; }
public static class FeatureFlag {
private boolean enabled;
private int rolloutPercentage;
// Getters and Setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public int getRolloutPercentage() { return rolloutPercentage; }
public void setRolloutPercentage(int rolloutPercentage) { this.rolloutPercentage = rolloutPercentage; }
}
}

Secrets Configuration

AppSecrets.java

package com.example.configreload.model;
import jakarta.validation.constraints.NotBlank;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
@ConfigurationProperties(prefix = "secrets")
@Validated
public class AppSecrets {
@NotBlank
private String databasePassword;
@NotBlank
private String apiKey;
@NotBlank
private String jwtSecret;
// Getters and Setters
public String getDatabasePassword() { return databasePassword; }
public void setDatabasePassword(String databasePassword) { this.databasePassword = databasePassword; }
public String getApiKey() { return apiKey; }
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
public String getJwtSecret() { return jwtSecret; }
public void setJwtSecret(String jwtSecret) { this.jwtSecret = jwtSecret; }
}

Step 4: File System Watcher Strategy

File System Configuration Watcher

FileSystemConfigWatcher.java

package com.example.configreload.watcher;
import com.example.configreload.event.ConfigReloadEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Component
public class FileSystemConfigWatcher {
private static final Logger logger = LoggerFactory.getLogger(FileSystemConfigWatcher.class);
private final ApplicationEventPublisher eventPublisher;
private final ScheduledExecutorService scheduler;
private WatchService watchService;
private boolean watching = false;
@Value("${app.config.path:/etc/app/config}")
private String configPath;
@Value("${app.secrets.path:/etc/app/secrets}")
private String secretsPath;
@Value("${app.config.reload.enabled:true}")
private boolean reloadEnabled;
@Value("${app.config.reload.interval:30000}")
private long reloadInterval;
public FileSystemConfigWatcher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "config-watcher");
t.setDaemon(true);
return t;
});
}
@PostConstruct
public void init() {
if (!reloadEnabled) {
logger.info("Config reload is disabled");
return;
}
try {
startWatching();
startPolling();
logger.info("Started config and secret watcher");
} catch (IOException e) {
logger.error("Failed to start config watcher", e);
}
}
@PreDestroy
public void destroy() {
watching = false;
scheduler.shutdown();
if (watchService != null) {
try {
watchService.close();
} catch (IOException e) {
logger.error("Error closing watch service", e);
}
}
}
private void startWatching() throws IOException {
watchService = FileSystems.getDefault().newWatchService();
Path configDir = Path.of(configPath);
Path secretsDir = Path.of(secretsPath);
if (Files.exists(configDir)) {
configDir.register(watchService, 
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE);
logger.info("Watching config directory: {}", configDir);
}
if (Files.exists(secretsDir)) {
secretsDir.register(watchService,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE);
logger.info("Watching secrets directory: {}", secretsDir);
}
watching = true;
// Start watch service in background thread
scheduler.execute(this::watchFiles);
}
private void startPolling() {
// Fallback polling mechanism
scheduler.scheduleAtFixedRate(this::checkForChanges, 
reloadInterval, reloadInterval, TimeUnit.MILLISECONDS);
}
private void watchFiles() {
while (watching) {
try {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) {
continue;
}
@SuppressWarnings("unchecked")
WatchEvent<Path> ev = (WatchEvent<Path>) event;
Path filename = ev.context();
logger.info("File change detected: {} - {}", kind.name(), filename);
// Determine what type of file changed
if (filename.toString().endsWith(".yaml") || filename.toString().endsWith(".yml")) {
publishReloadEvent("CONFIG", filename.toString());
} else {
publishReloadEvent("SECRET", filename.toString());
}
}
boolean valid = key.reset();
if (!valid) {
logger.warn("Watch key no longer valid");
break;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.info("Config watcher interrupted");
break;
} catch (ClosedWatchServiceException e) {
logger.info("Watch service closed");
break;
} catch (Exception e) {
logger.error("Error in file watcher", e);
}
}
}
private void checkForChanges() {
try {
// Check config files
checkDirectoryForChanges(configPath, "CONFIG");
// Check secret files
checkDirectoryForChanges(secretsPath, "SECRET");
} catch (Exception e) {
logger.error("Error checking for config changes", e);
}
}
private void checkDirectoryForChanges(String directoryPath, String type) {
File dir = new File(directoryPath);
if (!dir.exists() || !dir.isDirectory()) {
return;
}
File[] files = dir.listFiles();
if (files == null) return;
for (File file : files) {
long lastModified = file.lastModified();
Long previousModified = FileModificationCache.getLastModified(file.getAbsolutePath());
if (previousModified == null || lastModified > previousModified) {
logger.info("{} file changed: {}", type, file.getName());
FileModificationCache.updateLastModified(file.getAbsolutePath(), lastModified);
publishReloadEvent(type, file.getName());
}
}
}
private void publishReloadEvent(String configType, String fileName) {
ConfigReloadEvent event = new ConfigReloadEvent(this, configType, fileName);
eventPublisher.publishEvent(event);
}
// Cache for file modification times
private static class FileModificationCache {
private static final java.util.Map<String, Long> cache = new java.util.concurrent.ConcurrentHashMap<>();
public static Long getLastModified(String filePath) {
return cache.get(filePath);
}
public static void updateLastModified(String filePath, long lastModified) {
cache.put(filePath, lastModified);
}
public static void remove(String filePath) {
cache.remove(filePath);
}
}
}

Configuration Reload Event

ConfigReloadEvent.java

package com.example.configreload.event;
import org.springframework.context.ApplicationEvent;
public class ConfigReloadEvent extends ApplicationEvent {
private final String configType; // CONFIG or SECRET
private final String fileName;
private final long timestamp;
public ConfigReloadEvent(Object source, String configType, String fileName) {
super(source);
this.configType = configType;
this.fileName = fileName;
this.timestamp = System.currentTimeMillis();
}
// Getters
public String getConfigType() { return configType; }
public String getFileName() { return fileName; }
public long getTimestamp() { return timestamp; }
public boolean isConfigChange() {
return "CONFIG".equals(configType);
}
public boolean isSecretChange() {
return "SECRET".equals(configType);
}
}

Step 5: Kubernetes API Watcher Strategy

Kubernetes ConfigMap Watcher

KubernetesConfigWatcher.java

package com.example.configreload.watcher;
import com.example.configreload.event.ConfigReloadEvent;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.models.V1ConfigMap;
import io.kubernetes.client.openapi.models.V1ConfigMapList;
import io.kubernetes.client.openapi.models.V1Secret;
import io.kubernetes.client.openapi.models.V1SecretList;
import io.kubernetes.client.util.Config;
import io.kubernetes.client.util.Watch;
import okhttp3.OkHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Component
public class KubernetesConfigWatcher {
private static final Logger logger = LoggerFactory.getLogger(KubernetesConfigWatcher.class);
private final ApplicationEventPublisher eventPublisher;
private final ScheduledExecutorService scheduler;
private boolean watching = false;
@Value("${app.configmap.name:app-config}")
private String configMapName;
@Value("${app.secret.name:app-secrets}")
private String secretName;
@Value("${app.namespace:default}")
private String namespace;
@Value("${app.config.reload.enabled:true}")
private boolean reloadEnabled;
@Value("${kubernetes.watch.enabled:true}")
private boolean kubernetesWatchEnabled;
private CoreV1Api api;
public KubernetesConfigWatcher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
this.scheduler = Executors.newScheduledThreadPool(2, r -> {
Thread t = new Thread(r, "k8s-watcher");
t.setDaemon(true);
return t;
});
}
@PostConstruct
public void init() {
if (!reloadEnabled || !kubernetesWatchEnabled) {
logger.info("Kubernetes config watcher is disabled");
return;
}
try {
initializeKubernetesClient();
startWatching();
logger.info("Started Kubernetes config and secret watcher");
} catch (Exception e) {
logger.error("Failed to initialize Kubernetes watcher", e);
// Fall back to file system watching
}
}
@PreDestroy
public void destroy() {
watching = false;
scheduler.shutdown();
}
private void initializeKubernetesClient() throws IOException {
// Try to load from cluster first, then fall back to default
try {
ApiClient client = Config.fromCluster();
Configuration.setDefaultApiClient(client);
} catch (Exception e) {
logger.warn("Cannot load in-cluster config, falling back to default");
ApiClient client = Config.defaultClient();
Configuration.setDefaultApiClient(client);
}
// Configure timeout
OkHttpClient httpClient = Configuration.getDefaultApiClient().getHttpClient()
.newBuilder()
.readTimeout(0, TimeUnit.SECONDS) // No timeout for watch
.build();
Configuration.getDefaultApiClient().setHttpClient(httpClient);
this.api = new CoreV1Api();
}
private void startWatching() {
watching = true;
// Watch ConfigMap in separate thread
scheduler.execute(this::watchConfigMap);
// Watch Secret in separate thread
scheduler.execute(this::watchSecret);
}
private void watchConfigMap() {
while (watching) {
try {
logger.info("Starting ConfigMap watch: {}/{}", namespace, configMapName);
Watch<V1ConfigMap> watch = Watch.createWatch(
Configuration.getDefaultApiClient(),
api.listNamespacedConfigMapCall(
namespace,
null, // pretty
false, // allowWatchBookmarks
null, // _continue
null, // fieldSelector
"metadata.name=" + configMapName, // labelSelector
null, // limit
null, // resourceVersion
null, // resourceVersionMatch
null, // timeoutSeconds
true, // watch
null // callback
),
new Watch.ResponseType<V1ConfigMap>() {}
);
for (Watch.Response<V1ConfigMap> item : watch) {
if (!watching) break;
V1ConfigMap configMap = item.object;
String type = item.type;
logger.info("ConfigMap {}: {}", type, configMap.getMetadata().getName());
if ("MODIFIED".equals(type)) {
publishReloadEvent("CONFIG", "ConfigMap modified");
}
}
} catch (ApiException e) {
logger.error("Kubernetes API error watching ConfigMap", e);
sleepBeforeRetry();
} catch (Exception e) {
if (watching) {
logger.error("Error watching ConfigMap", e);
sleepBeforeRetry();
}
}
}
}
private void watchSecret() {
while (watching) {
try {
logger.info("Starting Secret watch: {}/{}", namespace, secretName);
Watch<V1Secret> watch = Watch.createWatch(
Configuration.getDefaultApiClient(),
api.listNamespacedSecretCall(
namespace,
null, // pretty
false, // allowWatchBookmarks
null, // _continue
null, // fieldSelector
"metadata.name=" + secretName, // labelSelector
null, // limit
null, // resourceVersion
null, // resourceVersionMatch
null, // timeoutSeconds
true, // watch
null // callback
),
new Watch.ResponseType<V1Secret>() {}
);
for (Watch.Response<V1Secret> item : watch) {
if (!watching) break;
V1Secret secret = item.object;
String type = item.type;
logger.info("Secret {}: {}", type, secret.getMetadata().getName());
if ("MODIFIED".equals(type)) {
publishReloadEvent("SECRET", "Secret modified");
}
}
} catch (ApiException e) {
logger.error("Kubernetes API error watching Secret", e);
sleepBeforeRetry();
} catch (Exception e) {
if (watching) {
logger.error("Error watching Secret", e);
sleepBeforeRetry();
}
}
}
}
private void sleepBeforeRetry() {
try {
Thread.sleep(30000); // Wait 30 seconds before retry
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void publishReloadEvent(String configType, String message) {
ConfigReloadEvent event = new ConfigReloadEvent(this, configType, message);
eventPublisher.publishEvent(event);
}
}

Step 6: Configuration Reload Service

Configuration Reload Service

ConfigReloadService.java

package com.example.configreload.service;
import com.example.configreload.event.ConfigReloadEvent;
import com.example.configreload.model.AppConfig;
import com.example.configreload.model.AppSecrets;
import com.example.configreload.model.FeatureFlags;
import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class ConfigReloadService {
private static final Logger logger = LoggerFactory.getLogger(ConfigReloadService.class);
private final ApplicationContext applicationContext;
private final Environment environment;
private final MeterRegistry meterRegistry;
private final AtomicInteger configReloadCount = new AtomicInteger(0);
private final AtomicInteger secretReloadCount = new AtomicInteger(0);
private final Map<String, Long> lastReloadTime = new ConcurrentHashMap<>();
@Autowired
@Qualifier("appConfig")
private AppConfig appConfig;
@Autowired(required = false)
@Qualifier("featureFlags")
private FeatureFlags featureFlags;
@Autowired(required = false)
@Qualifier("appSecrets")
private AppSecrets appSecrets;
public ConfigReloadService(ApplicationContext applicationContext, 
Environment environment,
MeterRegistry meterRegistry) {
this.applicationContext = applicationContext;
this.environment = environment;
this.meterRegistry = meterRegistry;
// Initialize metrics
initializeMetrics();
}
@Async
@EventListener
public void handleConfigReload(ConfigReloadEvent event) {
String configType = event.getConfigType();
String fileName = event.getFileName();
logger.info("Processing {} reload event for: {}", configType, fileName);
try {
switch (configType) {
case "CONFIG":
reloadConfiguration();
break;
case "SECRET":
reloadSecrets();
break;
default:
logger.warn("Unknown config type: {}", configType);
}
// Update metrics
updateReloadMetrics(configType, true);
} catch (Exception e) {
logger.error("Failed to reload {}: {}", configType, fileName, e);
updateReloadMetrics(configType, false);
}
}
private void reloadConfiguration() {
try {
// Reload AppConfig
if (appConfig != null) {
AppConfig newConfig = createNewAppConfig();
copyProperties(newConfig, appConfig);
logger.info("AppConfig reloaded successfully");
}
// Reload FeatureFlags
if (featureFlags != null) {
FeatureFlags newFlags = createNewFeatureFlags();
copyProperties(newFlags, featureFlags);
logger.info("FeatureFlags reloaded successfully");
}
// Refresh Spring beans that use @ConfigurationProperties
refreshConfigurationPropertiesBeans();
configReloadCount.incrementAndGet();
lastReloadTime.put("CONFIG", System.currentTimeMillis());
logger.info("Configuration reload completed successfully");
} catch (Exception e) {
logger.error("Failed to reload configuration", e);
throw new RuntimeException("Configuration reload failed", e);
}
}
private void reloadSecrets() {
try {
if (appSecrets != null) {
AppSecrets newSecrets = createNewAppSecrets();
copyProperties(newSecrets, appSecrets);
logger.info("Secrets reloaded successfully");
}
secretReloadCount.incrementAndGet();
lastReloadTime.put("SECRET", System.currentTimeMillis());
logger.info("Secrets reload completed successfully");
} catch (Exception e) {
logger.error("Failed to reload secrets", e);
throw new RuntimeException("Secrets reload failed", e);
}
}
private AppConfig createNewAppConfig() {
// This would typically re-read from the file system
// For simplicity, we're recreating from environment
AppConfig config = new AppConfig();
// In a real implementation, you would parse the YAML file again
return config;
}
private FeatureFlags createNewFeatureFlags() {
FeatureFlags flags = new FeatureFlags();
// Re-parse feature flags YAML
return flags;
}
private AppSecrets createNewAppSecrets() {
AppSecrets secrets = new AppSecrets();
// Re-read secrets from file system
return secrets;
}
private void copyProperties(Object source, Object target) throws Exception {
Field[] fields = source.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
Object value = field.get(source);
if (value != null) {
field.set(target, value);
}
}
}
private void refreshConfigurationPropertiesBeans() {
String[] beanNames = applicationContext.getBeanNamesForAnnotation(ConfigurationProperties.class);
for (String beanName : beanNames) {
Object bean = applicationContext.getBean(beanName);
try {
// Trigger re-initialization
applicationContext.getAutowireCapableBeanFactory().autowireBean(bean);
logger.debug("Refreshed configuration properties bean: {}", beanName);
} catch (Exception e) {
logger.warn("Failed to refresh bean: {}", beanName, e);
}
}
}
private void initializeMetrics() {
meterRegistry.gauge("config.reload.count", configReloadCount);
meterRegistry.gauge("secret.reload.count", secretReloadCount);
}
private void updateReloadMetrics(String configType, boolean success) {
String status = success ? "success" : "failure";
meterRegistry.counter("config.reloads",
"type", configType.toLowerCase(),
"status", status
).increment();
}
// Public methods for monitoring
public int getConfigReloadCount() {
return configReloadCount.get();
}
public int getSecretReloadCount() {
return secretReloadCount.get();
}
public Long getLastReloadTime(String configType) {
return lastReloadTime.get(configType);
}
public Map<String, Object> getReloadStats() {
return Map.of(
"configReloadCount", configReloadCount.get(),
"secretReloadCount", secretReloadCount.get(),
"lastConfigReload", lastReloadTime.get("CONFIG"),
"lastSecretReload", lastReloadTime.get("SECRET")
);
}
}

Step 7: Spring Configuration

Configuration Beans

AppConfiguration.java

package com.example.configreload.config;
import com.example.configreload.model.AppConfig;
import com.example.configreload.model.AppSecrets;
import com.example.configreload.model.FeatureFlags;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Configuration
@EnableAsync
public class AppConfiguration {
@Bean
@ConfigurationProperties(prefix = "app")
public AppConfig appConfig() {
return new AppConfig();
}
@Bean
@ConfigurationProperties(prefix = "features")
public FeatureFlags featureFlags() {
return new FeatureFlags();
}
@Bean
@ConfigurationProperties(prefix = "secrets")
public AppSecrets appSecrets() {
return new AppSecrets();
}
@Bean
public String databasePassword() throws IOException {
// Read password from secret file
Path secretPath = Paths.get("/etc/app/secrets/database-password");
if (Files.exists(secretPath)) {
return Files.readString(secretPath).trim();
}
return "default-password"; // Fallback for local development
}
@Bean
public String apiKey() throws IOException {
// Read API key from secret file
Path secretPath = Paths.get("/etc/app/secrets/api-key");
if (Files.exists(secretPath)) {
return Files.readString(secretPath).trim();
}
return "default-api-key"; // Fallback for local development
}
@Bean
public String jwtSecret() throws IOException {
// Read JWT secret from secret file
Path secretPath = Paths.get("/etc/app/secrets/jwt-secret");
if (Files.exists(secretPath)) {
return Files.readString(secretPath).trim();
}
return "default-jwt-secret"; // Fallback for local development
}
}

Application Properties

application.yaml

# Application configuration
app:
name: "Config Reload Demo"
version: "1.0.0"
config:
reload:
enabled: true
interval: 30000
# Spring configuration
spring:
application:
name: config-reload-demo
config:
import: 
- file:/etc/app/config/application.yaml
- file:/etc/app/config/feature-flags.yaml
profiles:
active: kubernetes
# Actuator endpoints
management:
endpoints:
web:
exposure:
include: health,info,metrics,configprops,env,reload
endpoint:
health:
show-details: always
show-components: always
reload:
enabled: true
info:
env:
enabled: true
# Logging
logging:
level:
com.example.configreload: INFO
org.springframework: WARN
pattern:
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
# Kubernetes
kubernetes:
watch:
enabled: true

Step 8: REST API for Configuration Management

Configuration Controller

ConfigController.java

package com.example.configreload.controller;
import com.example.configreload.model.AppConfig;
import com.example.configreload.model.AppSecrets;
import com.example.configreload.model.FeatureFlags;
import com.example.configreload.service.ConfigReloadService;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/config")
public class ConfigController {
private final ApplicationContext applicationContext;
private final ConfigReloadService configReloadService;
private final AppConfig appConfig;
private final FeatureFlags featureFlags;
public ConfigController(ApplicationContext applicationContext,
ConfigReloadService configReloadService,
AppConfig appConfig,
FeatureFlags featureFlags) {
this.applicationContext = applicationContext;
this.configReloadService = configReloadService;
this.appConfig = appConfig;
this.featureFlags = featureFlags;
}
@GetMapping("/current")
public Map<String, Object> getCurrentConfig() {
return Map.of(
"appConfig", appConfig,
"featureFlags", featureFlags
);
}
@GetMapping("/reload/stats")
public Map<String, Object> getReloadStats() {
return configReloadService.getReloadStats();
}
@PostMapping("/reload")
public Map<String, String> triggerReload(@RequestParam(defaultValue = "CONFIG") String type) {
try {
switch (type.toUpperCase()) {
case "CONFIG":
configReloadService.reloadConfiguration();
break;
case "SECRET":
configReloadService.reloadSecrets();
break;
default:
return Map.of("status", "error", "message", "Invalid type: " + type);
}
return Map.of("status", "success", "message", type + " reload triggered");
} catch (Exception e) {
return Map.of("status", "error", "message", e.getMessage());
}
}
@GetMapping("/beans")
public Map<String, String> getConfigurationBeans() {
String[] beanNames = applicationContext.getBeanNamesForAnnotation(ConfigurationProperties.class);
return Map.of(
"configurationPropertiesBeans", String.join(", ", beanNames)
);
}
}

Actuator Custom Endpoint

ReloadEndpoint.java

package com.example.configreload.actuator;
import com.example.configreload.service.ConfigReloadService;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@Endpoint(id = "reload")
public class ReloadEndpoint {
private final ConfigReloadService configReloadService;
public ReloadEndpoint(ConfigReloadService configReloadService) {
this.configReloadService = configReloadService;
}
@ReadOperation
public Map<String, Object> reloadInfo() {
return configReloadService.getReloadStats();
}
@WriteOperation
public Map<String, String> triggerReload(String type) {
try {
if ("config".equalsIgnoreCase(type)) {
configReloadService.reloadConfiguration();
return Map.of("status", "success", "message", "Configuration reloaded");
} else if ("secret".equalsIgnoreCase(type)) {
configReloadService.reloadSecrets();
return Map.of("status", "success", "message", "Secrets reloaded");
} else {
return Map.of("status", "error", "message", "Invalid type. Use 'config' or 'secret'");
}
} catch (Exception e) {
return Map.of("status", "error", "message", e.getMessage());
}
}
}

Step 9: Usage Examples

Database Service with Config Reload

DatabaseService.java

package com.example.configreload.service;
import com.example.configreload.model.AppConfig;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
@Service
public class DatabaseService {
private static final Logger logger = LoggerFactory.getLogger(DatabaseService.class);
private final AppConfig appConfig;
private final String databasePassword;
private volatile HikariDataSource dataSource;
public DatabaseService(@Qualifier("appConfig") AppConfig appConfig,
@Qualifier("databasePassword") String databasePassword) {
this.appConfig = appConfig;
this.databasePassword = databasePassword;
this.dataSource = createDataSource();
}
public DataSource getDataSource() {
return dataSource;
}
@Async
@EventListener
public void handleConfigReload(com.example.configreload.event.ConfigReloadEvent event) {
if (event.isConfigChange() || event.isSecretChange()) {
logger.info("Recreating database connection pool due to config change");
recreateDataSource();
}
}
private synchronized void recreateDataSource() {
if (dataSource != null && !dataSource.isClosed()) {
dataSource.close();
}
dataSource = createDataSource();
logger.info("Database connection pool recreated");
}
private HikariDataSource createDataSource() {
var dbConfig = appConfig.getDatabase();
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl(dbConfig.getUrl());
hikariConfig.setUsername("app_user"); // From config
hikariConfig.setPassword(databasePassword); // From secret
hikariConfig.setDriverClassName("org.postgresql.Driver");
// Connection pool configuration
hikariConfig.setMinimumIdle(dbConfig.getMinSize());
hikariConfig.setMaximumPoolSize(dbConfig.getMaxSize());
hikariConfig.setConnectionTimeout(dbConfig.getTimeout());
hikariConfig.setIdleTimeout(600000);
hikariConfig.setPoolName("AppConnectionPool");
logger.info("Creating database connection pool for: {}", dbConfig.getUrl());
return new HikariDataSource(hikariConfig);
}
}

Feature Flag Service

FeatureFlagService.java

package com.example.configreload.service;
import com.example.configreload.model.FeatureFlags;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
@Service
public class FeatureFlagService {
private static final Logger logger = LoggerFactory.getLogger(FeatureFlagService.class);
private final FeatureFlags featureFlags;
public FeatureFlagService(@Qualifier("featureFlags") FeatureFlags featureFlags) {
this.featureFlags = featureFlags;
}
public boolean isFeatureEnabled(String featureName) {
return Optional.ofNullable(featureFlags.getFeatures().get(featureName))
.map(FeatureFlags.FeatureFlag::isEnabled)
.orElse(false);
}
public boolean isFeatureEnabledForUser(String featureName, String userId) {
var feature = featureFlags.getFeatures().get(featureName);
if (feature == null || !feature.isEnabled()) {
return false;
}
// Check rollout percentage
if (feature.getRolloutPercentage() < 100) {
double random = ThreadLocalRandom.current().nextDouble(100.0);
return random < feature.getRolloutPercentage();
}
return true;
}
@Async
@EventListener
public void handleConfigReload(com.example.configreload.event.ConfigReloadEvent event) {
if (event.isConfigChange()) {
logger.info("Feature flags updated. New flags: {}", featureFlags.getFeatures().keySet());
}
}
public FeatureFlags getFeatureFlags() {
return featureFlags;
}
}

Step 10: Testing

Unit Tests

ConfigReloadServiceTest.java

package com.example.configreload.service;
import com.example.configreload.event.ConfigReloadEvent;
import com.example.configreload.model.AppConfig;
import com.example.configreload.model.FeatureFlags;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationContext;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class ConfigReloadServiceTest {
@Mock
private ApplicationContext applicationContext;
@Mock
private org.springframework.core.env.Environment environment;
@Mock
private io.micrometer.core.instrument.MeterRegistry meterRegistry;
private ConfigReloadService configReloadService;
private AppConfig appConfig;
private FeatureFlags featureFlags;
@BeforeEach
void setUp() {
appConfig = new AppConfig();
featureFlags = new FeatureFlags();
configReloadService = new ConfigReloadService(
applicationContext, environment, meterRegistry);
// Use reflection to set the fields for testing
setField(configReloadService, "appConfig", appConfig);
setField(configReloadService, "featureFlags", featureFlags);
}
@Test
void testHandleConfigReload() {
ConfigReloadEvent event = new ConfigReloadEvent(this, "CONFIG", "test.yaml");
assertDoesNotThrow(() -> configReloadService.handleConfigReload(event));
assertEquals(1, configReloadService.getConfigReloadCount());
}
@Test
void testGetReloadStats() {
var stats = configReloadService.getReloadStats();
assertNotNull(stats);
assertTrue(stats.containsKey("configReloadCount"));
assertTrue(stats.containsKey("secretReloadCount"));
}
private void setField(Object target, String fieldName, Object value) {
try {
var field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

Integration Test

ConfigReloadIntegrationTest.java

package com.example.configreload;
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.ActiveProfiles;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class ConfigReloadIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void testConfigEndpoint() {
ResponseEntity<Map> response = restTemplate.getForEntity(
"http://localhost:" + port + "/api/v1/config/current", Map.class);
assertEquals(200, response.getStatusCodeValue());
assertNotNull(response.getBody());
assertTrue(response.getBody().containsKey("appConfig"));
}
@Test
void testReloadStatsEndpoint() {
ResponseEntity<Map> response = restTemplate.getForEntity(
"http://localhost:" + port + "/api/v1/config/reload/stats", Map.class);
assertEquals(200, response.getStatusCodeValue());
assertNotNull(response.getBody());
}
}

Best Practices

1. Security Considerations

  • Never log sensitive configuration values
  • Use Kubernetes RBAC to restrict access to Secrets
  • Encrypt sensitive data at rest and in transit
  • Regularly rotate secrets and certificates

2. Performance Optimization

  • Use efficient file watching mechanisms
  • Implement debouncing for frequent changes
  • Cache configuration values appropriately
  • Monitor memory usage during reloads

3. Reliability

  • Implement circuit breakers for configuration reloads
  • Maintain fallback configuration values
  • Test configuration changes in staging first
  • Implement rollback mechanisms

4. Monitoring

  • Track configuration reload success/failure rates
  • Monitor application behavior after configuration changes
  • Set up alerts for configuration reload failures
  • Log configuration changes for audit purposes

Conclusion

This comprehensive ConfigMap and Secret reloading implementation provides:

  • Multiple Strategies: File system watching, Kubernetes API watching, and polling
  • Spring Integration: Seamless integration with Spring Boot configuration properties
  • Type Safety: Strongly typed configuration models with validation
  • Monitoring: Comprehensive metrics and health checks
  • Security: Proper handling of sensitive data
  • Reliability: Fallback mechanisms and error handling

Key Benefits:

  1. Zero Downtime: Reload configuration without restarting the application
  2. Real-time Updates: Immediate propagation of configuration changes
  3. Kubernetes Native: Full integration with Kubernetes ecosystem
  4. Observability: Comprehensive monitoring and metrics
  5. Developer Experience: Simple API for configuration management

By implementing this solution, you can achieve dynamic configuration management that scales with your application's needs while maintaining security and reliability.

Leave a Reply

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


Macro Nepal Helper