Spring Cloud Config in Java: Comprehensive Configuration Management Guide

Spring Cloud Config provides centralized external configuration management backed by a Git repository. It enables distributed systems to have centralized configuration with environment-specific overrides.


Core Concepts

What is Spring Cloud Config?

  • Centralized configuration server for distributed systems
  • Git-backed configuration repository
  • Environment-specific configuration management
  • Configuration versioning and audit trails
  • Dynamic configuration refresh without restarts

Key Benefits:

  • Centralized Management: Single source of truth for all configurations
  • Environment Specific: Different configurations per environment (dev, staging, prod)
  • Version Control: Full history and rollback capabilities via Git
  • Dynamic Refresh: Update configurations without application restart
  • Encryption: Support for encrypting sensitive configuration values

Architecture Overview

Client Applications
↓
Spring Cloud Config Server
↓
Git Repository (Configuration Files)
↓
Optional: Vault/Consul for Secrets

Dependencies and Setup

Maven Dependencies for Config Server
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<spring-cloud.version>2022.0.3</spring-cloud.version>
</properties>
<dependencies>
<!-- Spring Cloud Config Server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!-- 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-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Encryption Support -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-rsa</artifactId>
<version>1.0.11.RELEASE</version>
</dependency>
<!-- Monitoring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Maven Dependencies for Config Client
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<spring-cloud.version>2022.0.3</spring-cloud.version>
</properties>
<dependencies>
<!-- Spring Cloud Config Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!-- 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-data-jpa</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Refresh Scope -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Config Server Implementation

1. Config Server Main Application
@SpringBootApplication
@EnableConfigServer
@EnableDiscoveryClient
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
2. Config Server Configuration
# application.yml
server:
port: 8888
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://github.com/your-org/config-repo.git
default-label: main
search-paths: 
- '{application}'
- '{profile}'
- common
timeout: 10
force-pull: true
encrypt:
enabled: true
composite:
- type: git
uri: https://github.com/your-org/config-repo.git
- type: vault
host: 127.0.0.1
port: 8200
kv-version: 2
security:
user:
name: config-user
password: ${CONFIG_SERVER_PASSWORD:changeme}
# Encryption Configuration
encrypt:
key-store:
location: classpath:keystore.jks
password: changeit
alias: configKey
secret: changeit
# Management Endpoints
management:
endpoints:
web:
exposure:
include: health,info,metrics,configprops,env,refresh,busrefresh
endpoint:
health:
show-details: always
refresh:
enabled: true
busrefresh:
enabled: true
# Logging
logging:
level:
org.springframework.cloud.config: DEBUG
3. Security Configuration for Config Server
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/actuator/info").permitAll()
.anyRequest().authenticated()
)
.httpBasic(withDefaults())
.csrf(csrf -> csrf.disable());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withUsername("config-user")
.password("{noop}changeme")
.roles("CONFIG_READER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
4. Custom Environment Repository
@Component
@Slf4j
public class CustomGitEnvironmentRepository implements EnvironmentRepository {
private final GitEnvironmentRepository delegate;
private final RedisTemplate<String, String> redisTemplate;
public CustomGitEnvironmentRepository(GitEnvironmentRepository delegate,
RedisTemplate<String, String> redisTemplate) {
this.delegate = delegate;
this.redisTemplate = redisTemplate;
}
@Override
public Environment findOne(String application, String profile, String label) {
String cacheKey = String.format("config:%s:%s:%s", application, profile, label);
// Try cache first
String cachedConfig = redisTemplate.opsForValue().get(cacheKey);
if (cachedConfig != null) {
log.debug("Returning cached configuration for: {}/{}/{}", application, profile, label);
return deserializeEnvironment(cachedConfig);
}
// Fetch from Git repository
Environment environment = delegate.findOne(application, profile, label);
// Cache the result
try {
String serialized = serializeEnvironment(environment);
redisTemplate.opsForValue().set(cacheKey, serialized, Duration.ofMinutes(5));
} catch (Exception e) {
log.warn("Failed to cache configuration", e);
}
return environment;
}
private String serializeEnvironment(Environment environment) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(environment);
}
private Environment deserializeEnvironment(String json) {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(json, Environment.class);
} catch (JsonProcessingException e) {
log.warn("Failed to deserialize cached environment", e);
return null;
}
}
}
5. Encryption/Decryption Service
@Service
@Slf4j
public class ConfigEncryptionService {
private final TextEncryptor textEncryptor;
private final ObjectMapper objectMapper;
public ConfigEncryptionService(@Qualifier("configTextEncryptor") TextEncryptor textEncryptor) {
this.textEncryptor = textEncryptor;
this.objectMapper = new ObjectMapper();
}
public String encrypt(String plainText) {
if (plainText == null) {
return null;
}
try {
if (isAlreadyEncrypted(plainText)) {
return plainText;
}
return "{cipher}" + textEncryptor.encrypt(plainText);
} catch (Exception e) {
log.error("Failed to encrypt value", e);
throw new ConfigEncryptionException("Encryption failed", e);
}
}
public String decrypt(String encryptedText) {
if (encryptedText == null) {
return null;
}
try {
if (!isEncrypted(encryptedText)) {
return encryptedText;
}
String cipherText = encryptedText.substring("{cipher}".length());
return textEncryptor.decrypt(cipherText);
} catch (Exception e) {
log.error("Failed to decrypt value: {}", encryptedText, e);
throw new ConfigDecryptionException("Decryption failed", e);
}
}
public Map<String, String> decryptProperties(Map<String, String> properties) {
if (properties == null) {
return Collections.emptyMap();
}
Map<String, String> decrypted = new HashMap<>();
properties.forEach((key, value) -> {
try {
decrypted.put(key, decrypt(value));
} catch (Exception e) {
log.warn("Failed to decrypt property: {}", key, e);
decrypted.put(key, value); // Keep original value on decryption failure
}
});
return decrypted;
}
public boolean isEncrypted(String value) {
return value != null && value.startsWith("{cipher}");
}
private boolean isAlreadyEncrypted(String value) {
return isEncrypted(value);
}
public HealthCheckResult healthCheck() {
try {
String testValue = "config-server-health-check";
String encrypted = encrypt(testValue);
String decrypted = decrypt(encrypted);
boolean healthy = testValue.equals(decrypted);
String message = healthy ? "Encryption/decryption working correctly" : "Encryption test failed";
return new HealthCheckResult(healthy, message, Map.of(
"testValue", testValue,
"encrypted", encrypted,
"decrypted", decrypted
));
} catch (Exception e) {
return new HealthCheckResult(false, "Encryption health check failed: " + e.getMessage(), Map.of());
}
}
}
@Configuration
public class EncryptionConfig {
@Bean
@ConditionalOnProperty(name = "encrypt.key-store.location")
public TextEncryptor configTextEncryptor(
@Value("${encrypt.key-store.location}") String keyStoreLocation,
@Value("${encrypt.key-store.password}") String keyStorePassword,
@Value("${encrypt.key-store.alias}") String keyStoreAlias,
@Value("${encrypt.key-store.secret}") String keyStoreSecret) {
try {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
new ClassPathResource(keyStoreLocation.replace("classpath:", "")),
keyStorePassword.toCharArray()
);
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(keyStoreAlias, keyStoreSecret.toCharArray());
return new RsaTextEncryptor(keyPair);
} catch (Exception e) {
throw new IllegalStateException("Failed to initialize text encryptor", e);
}
}
}
6. Config Server Controller
@RestController
@RequestMapping("/admin")
@Slf4j
public class ConfigAdminController {
private final ConfigEncryptionService encryptionService;
private final EnvironmentRepository environmentRepository;
public ConfigAdminController(ConfigEncryptionService encryptionService,
EnvironmentRepository environmentRepository) {
this.encryptionService = encryptionService;
this.environmentRepository = environmentRepository;
}
@PostMapping("/encrypt")
public ResponseEntity<EncryptionResult> encrypt(@RequestBody EncryptionRequest request) {
try {
String encrypted = encryptionService.encrypt(request.getPlainText());
return ResponseEntity.ok(new EncryptionResult(encrypted, true, "Encryption successful"));
} catch (Exception e) {
log.error("Encryption failed", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new EncryptionResult(null, false, "Encryption failed: " + e.getMessage()));
}
}
@PostMapping("/decrypt")
public ResponseEntity<DecryptionResult> decrypt(@RequestBody DecryptionRequest request) {
try {
String decrypted = encryptionService.decrypt(request.getEncryptedText());
return ResponseEntity.ok(new DecryptionResult(decrypted, true, "Decryption successful"));
} catch (Exception e) {
log.error("Decryption failed", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new DecryptionResult(null, false, "Decryption failed: " + e.getMessage()));
}
}
@GetMapping("/config/{application}/{profile}/{label}")
public ResponseEntity<Environment> getConfig(
@PathVariable String application,
@PathVariable String profile,
@PathVariable String label) {
try {
Environment environment = environmentRepository.findOne(application, profile, label);
return ResponseEntity.ok(environment);
} catch (Exception e) {
log.error("Failed to get configuration for {}/{}/{}", application, profile, label, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/health/encryption")
public ResponseEntity<HealthCheckResult> encryptionHealth() {
HealthCheckResult health = encryptionService.healthCheck();
HttpStatus status = health.isHealthy() ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE;
return ResponseEntity.status(status).body(health);
}
}
@Data
class EncryptionRequest {
private String plainText;
}
@Data
class EncryptionResult {
private final String encryptedText;
private final boolean success;
private final String message;
}
@Data
class DecryptionRequest {
private String encryptedText;
}
@Data
class DecryptionResult {
private final String decryptedText;
private final boolean success;
private final String message;
}
@Data
class HealthCheckResult {
private final boolean healthy;
private final String message;
private final Map<String, Object> details;
}

Config Client Implementation

1. Client Application Configuration
# bootstrap.yml
spring:
application:
name: user-service
profiles:
active: dev
cloud:
config:
uri: http://config-server:8888
fail-fast: true
retry:
initial-interval: 1000
max-interval: 2000
max-attempts: 6
request-connect-timeout: 5000
request-read-timeout: 5000
username: config-user
password: ${CONFIG_SERVER_PASSWORD:changeme}
label: main
profile: dev,common
bus:
enabled: true
# Application specific configuration
app:
config:
refresh-interval: 300000  # 5 minutes
encryption-enabled: true
2. Configuration Properties Classes
@Configuration
@ConfigurationProperties(prefix = "app.database")
@Data
@RefreshScope
public class DatabaseConfig {
private String url;
private String username;
private String password;
private String driverClassName;
private int maxPoolSize = 10;
private int minIdle = 2;
private long connectionTimeout = 30000;
private long idleTimeout = 300000;
private long maxLifetime = 1800000;
@PostConstruct
public void validate() {
if (url == null || url.trim().isEmpty()) {
throw new IllegalStateException("Database URL must be configured");
}
}
}
@Configuration
@ConfigurationProperties(prefix = "app.security")
@Data
@RefreshScope
public class SecurityConfig {
private String apiKey;
private List<String> allowedOrigins = Arrays.asList("*");
private boolean csrfEnabled = true;
private int jwtExpirationMs = 86400000; // 24 hours
private String jwtSecret;
@PostConstruct
public void validate() {
if (jwtSecret == null || jwtSecret.trim().isEmpty()) {
throw new IllegalStateException("JWT secret must be configured");
}
}
}
@Configuration
@ConfigurationProperties(prefix = "app.feature")
@Data
@RefreshScope
public class FeatureConfig {
private boolean newUserRegistrationEnabled = true;
private boolean emailVerificationRequired = false;
private int maxUsersPerAccount = 10;
private boolean advancedAnalyticsEnabled = false;
private Map<String, Boolean> flags = new HashMap<>();
public boolean isFeatureEnabled(String feature) {
return flags.getOrDefault(feature, false);
}
}
@Configuration
@ConfigurationProperties(prefix = "app.external")
@Data
@RefreshScope
public class ExternalServiceConfig {
private String paymentServiceUrl;
private String emailServiceUrl;
private String notificationServiceUrl;
private int timeoutMs = 5000;
private int maxRetries = 3;
private Map<String, String> headers = new HashMap<>();
}
3. Dynamic Configuration Service
@Service
@Slf4j
public class DynamicConfigService {
private final DatabaseConfig databaseConfig;
private final SecurityConfig securityConfig;
private final FeatureConfig featureConfig;
private final ExternalServiceConfig externalServiceConfig;
private final ContextRefresher contextRefresher;
private final Map<String, Object> configCache = new ConcurrentHashMap<>();
private final AtomicReference<Instant> lastRefresh = new AtomicReference<>(Instant.now());
public DynamicConfigService(DatabaseConfig databaseConfig,
SecurityConfig securityConfig,
FeatureConfig featureConfig,
ExternalServiceConfig externalServiceConfig,
ContextRefresher contextRefresher) {
this.databaseConfig = databaseConfig;
this.securityConfig = securityConfig;
this.featureConfig = featureConfig;
this.externalServiceConfig = externalServiceConfig;
this.contextRefresher = contextRefresher;
initializeCache();
}
public DatabaseConfig getDatabaseConfig() {
return databaseConfig;
}
public SecurityConfig getSecurityConfig() {
return securityConfig;
}
public FeatureConfig getFeatureConfig() {
return featureConfig;
}
public ExternalServiceConfig getExternalServiceConfig() {
return externalServiceConfig;
}
public Map<String, Object> getAllConfig() {
Map<String, Object> config = new HashMap<>();
config.put("database", databaseConfig);
config.put("security", securityConfig);
config.put("features", featureConfig);
config.put("externalServices", externalServiceConfig);
config.put("lastRefresh", lastRefresh.get());
return config;
}
public void refreshConfiguration() {
try {
log.info("Refreshing configuration from Config Server");
Set<String> refreshed = contextRefresher.refresh();
if (!refreshed.isEmpty()) {
log.info("Refreshed configuration properties: {}", refreshed);
lastRefresh.set(Instant.now());
// Update cache
initializeCache();
// Notify listeners
notifyConfigChangeListeners(refreshed);
} else {
log.info("No configuration properties were refreshed");
}
} catch (Exception e) {
log.error("Failed to refresh configuration", e);
throw new ConfigRefreshException("Configuration refresh failed", e);
}
}
public boolean isFeatureEnabled(String feature) {
return featureConfig.isFeatureEnabled(feature);
}
public <T> T getConfigValue(String key, Class<T> type) {
return type.cast(configCache.get(key));
}
public Instant getLastRefreshTime() {
return lastRefresh.get();
}
private void initializeCache() {
configCache.clear();
configCache.put("database.url", databaseConfig.getUrl());
configCache.put("security.apiKey", securityConfig.getApiKey());
configCache.put("feature.newUserRegistration", featureConfig.isNewUserRegistrationEnabled());
configCache.put("external.paymentServiceUrl", externalServiceConfig.getPaymentServiceUrl());
}
private void notifyConfigChangeListeners(Set<String> changedProperties) {
// In a real implementation, you might use ApplicationEventPublisher
// to publish configuration change events
log.debug("Configuration changed: {}", changedProperties);
}
}
4. Configuration-Enabled DataSource
@Configuration
@Slf4j
public class DynamicDataSourceConfig {
@Bean
@RefreshScope
public DataSource dataSource(DatabaseConfig databaseConfig) {
log.info("Creating DataSource with URL: {}", databaseConfig.getUrl());
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(databaseConfig.getUrl());
dataSource.setUsername(databaseConfig.getUsername());
dataSource.setPassword(databaseConfig.getPassword());
dataSource.setDriverClassName(databaseConfig.getDriverClassName());
dataSource.setMaximumPoolSize(databaseConfig.getMaxPoolSize());
dataSource.setMinimumIdle(databaseConfig.getMinIdle());
dataSource.setConnectionTimeout(databaseConfig.getConnectionTimeout());
dataSource.setIdleTimeout(databaseConfig.getIdleTimeout());
dataSource.setMaxLifetime(databaseConfig.getMaxLifetime());
// Additional HikariCP optimizations
dataSource.setLeakDetectionThreshold(60000);
dataSource.setConnectionTestQuery("SELECT 1");
return dataSource;
}
@Bean
@RefreshScope
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.example.entity");
em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
Properties properties = new Properties();
properties.setProperty("hibernate.hbm2ddl.auto", "validate");
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
properties.setProperty("hibernate.show_sql", "false");
properties.setProperty("hibernate.format_sql", "true");
em.setJpaProperties(properties);
return em;
}
}
5. Configuration Refresh Controller
@RestController
@RequestMapping("/api/config")
@Slf4j
public class ConfigRefreshController {
private final DynamicConfigService configService;
private final ApplicationEventPublisher eventPublisher;
public ConfigRefreshController(DynamicConfigService configService,
ApplicationEventPublisher eventPublisher) {
this.configService = configService;
this.eventPublisher = eventPublisher;
}
@PostMapping("/refresh")
public ResponseEntity<ConfigRefreshResult> refreshConfig() {
try {
configService.refreshConfiguration();
ConfigRefreshResult result = new ConfigRefreshResult(
true,
"Configuration refreshed successfully",
configService.getLastRefreshTime()
);
return ResponseEntity.ok(result);
} catch (ConfigRefreshException e) {
log.error("Configuration refresh failed", e);
ConfigRefreshResult result = new ConfigRefreshResult(
false,
"Configuration refresh failed: " + e.getMessage(),
configService.getLastRefreshTime()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
@GetMapping("/current")
public ResponseEntity<Map<String, Object>> getCurrentConfig() {
Map<String, Object> config = configService.getAllConfig();
return ResponseEntity.ok(config);
}
@GetMapping("/features/{feature}")
public ResponseEntity<FeatureStatus> getFeatureStatus(@PathVariable String feature) {
boolean enabled = configService.isFeatureEnabled(feature);
FeatureStatus status = new FeatureStatus(
feature,
enabled,
configService.getLastRefreshTime()
);
return ResponseEntity.ok(status);
}
@PostMapping("/features/{feature}/toggle")
public ResponseEntity<FeatureToggleResult> toggleFeature(@PathVariable String feature) {
// Note: This would typically update the configuration in the Git repository
// For demonstration purposes only
FeatureToggleResult result = new FeatureToggleResult(
feature,
!configService.isFeatureEnabled(feature),
"Feature toggle would update configuration repository"
);
return ResponseEntity.ok(result);
}
}
@Data
class ConfigRefreshResult {
private final boolean success;
private final String message;
private final Instant lastRefresh;
}
@Data
class FeatureStatus {
private final String featureName;
private final boolean enabled;
private final Instant lastRefreshed;
}
@Data
class FeatureToggleResult {
private final String featureName;
private final boolean newState;
private final String message;
}
6. Configuration Change Event Listener
@Component
@Slf4j
public class ConfigChangeEventListener {
private final DynamicConfigService configService;
public ConfigChangeEventListener(DynamicConfigService configService) {
this.configService = configService;
}
@EventListener
public void handleConfigChange(EnvironmentChangeEvent event) {
log.info("Configuration changed: {}", event.getKeys());
// Handle specific configuration changes
event.getKeys().forEach(key -> {
switch (key) {
case "app.feature.newUserRegistrationEnabled":
handleNewUserRegistrationChange();
break;
case "app.security.apiKey":
handleApiKeyChange();
break;
case "app.database.url":
handleDatabaseUrlChange();
break;
default:
log.debug("Configuration changed for key: {}", key);
}
});
}
@EventListener
public void handleRefreshScopeRefreshed(RefreshScopeRefreshedEvent event) {
log.info("Refresh scope was refreshed");
// Perform any necessary cleanup or reinitialization
}
private void handleNewUserRegistrationChange() {
boolean enabled = configService.getFeatureConfig().isNewUserRegistrationEnabled();
log.info("New user registration feature is now: {}", enabled ? "ENABLED" : "DISABLED");
// Notify other components or perform actions based on this change
}
private void handleApiKeyChange() {
log.info("API key was updated");
// Invalidate cached API keys or notify security components
}
private void handleDatabaseUrlChange() {
log.warn("Database URL was changed. Application restart may be required for DataSource reinitialization.");
// In a production scenario, you might want to trigger a DataSource recreation
}
}
7. Health Indicator for Config Client
@Component
public class ConfigClientHealthIndicator implements HealthIndicator {
private final DynamicConfigService configService;
private final ConfigClientProperties configClientProperties;
public ConfigClientHealthIndicator(DynamicConfigService configService,
ConfigClientProperties configClientProperties) {
this.configService = configService;
this.configClientProperties = configClientProperties;
}
@Override
public Health health() {
try {
Map<String, Object> details = new HashMap<>();
details.put("configServerUri", configClientProperties.getUri());
details.put("applicationName", configClientProperties.getName());
details.put("profiles", configClientProperties.getProfile());
details.put("label", configClientProperties.getLabel());
details.put("lastRefresh", configService.getLastRefreshTime());
// Check if essential configurations are present
boolean configValid = isConfigurationValid();
details.put("configurationValid", configValid);
if (configValid) {
return Health.up()
.withDetails(details)
.build();
} else {
return Health.down()
.withDetail("error", "Essential configuration missing")
.withDetails(details)
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
private boolean isConfigurationValid() {
try {
DatabaseConfig dbConfig = configService.getDatabaseConfig();
SecurityConfig securityConfig = configService.getSecurityConfig();
return dbConfig.getUrl() != null && 
securityConfig.getJwtSecret() != null;
} catch (Exception e) {
return false;
}
}
}

Git Repository Structure

config-repo/
├── application.yml
├── user-service/
│   ├── application.yml
│   ├── application-dev.yml
│   ├── application-staging.yml
│   └── application-prod.yml
├── order-service/
│   ├── application.yml
│   ├── application-dev.yml
│   └── application-prod.yml
└── common/
├── database.yml
├── security.yml
└── features.yml
Example Configuration Files
# config-repo/application.yml
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: UTC
management:
endpoints:
web:
exposure:
include: health,info,metrics,refresh
endpoint:
health:
show-details: always
logging:
level:
com.example: INFO
# config-repo/user-service/application-dev.yml
app:
database:
url: jdbc:postgresql://localhost:5432/userdb_dev
username: dev_user
password: dev_password
max-pool-size: 5
security:
api-key: dev-api-key-123
jwt-secret: dev-jwt-secret-456
allowed-origins:
- http://localhost:3000
- http://localhost:8080
feature:
new-user-registration-enabled: true
email-verification-required: false
max-users-per-account: 50
external:
payment-service-url: http://localhost:8081
email-service-url: http://localhost:8082
timeout-ms: 3000
# config-repo/user-service/application-prod.yml
app:
database:
url: jdbc:postgresql://prod-db:5432/userdb
username: ${DB_USERNAME}
password: '{cipher}AQB...encrypted...XYZ'
max-pool-size: 20
security:
api-key: '{cipher}AQB...encrypted...XYZ'
jwt-secret: '{cipher}AQB...encrypted...XYZ'
allowed-origins:
- https://myapp.com
feature:
new-user-registration-enabled: true
email-verification-required: true
max-users-per-account: 1000
external:
payment-service-url: https://payment.myapp.com
email-service-url: https://email.myapp.com
timeout-ms: 10000

Testing

1. Config Server Test
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {
"spring.cloud.config.server.git.uri=file:./src/test/resources/config-repo",
"spring.cloud.config.server.git.default-label=main"
})
class ConfigServerApplicationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testConfigServerEndpoints() {
ResponseEntity<Environment> response = restTemplate
.withBasicAuth("config-user", "changeme")
.getForEntity("/user-service/dev/main", Environment.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
}
@Test
void testEncryptionEndpoint() {
EncryptionRequest request = new EncryptionRequest();
request.setPlainText("secret-value");
ResponseEntity<EncryptionResult> response = restTemplate
.withBasicAuth("config-user", "changeme")
.postForEntity("/admin/encrypt", request, EncryptionResult.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().isSuccess()).isTrue();
assertThat(response.getBody().getEncryptedText()).startsWith("{cipher}");
}
}
2. Config Client Test
@SpringBootTest
@ActiveProfiles("test")
@TestPropertySource(properties = {
"spring.cloud.config.enabled=false",
"app.database.url=jdbc:h2:mem:testdb",
"app.security.jwt-secret=test-secret"
})
class ConfigClientTest {
@Autowired
private DynamicConfigService configService;
@Autowired
private DatabaseConfig databaseConfig;
@Test
void testConfigurationLoading() {
assertThat(databaseConfig.getUrl()).isEqualTo("jdbc:h2:mem:testdb");
assertThat(configService.getFeatureConfig().isNewUserRegistrationEnabled()).isTrue();
}
@Test
void testFeatureToggle() {
assertThat(configService.isFeatureEnabled("newUserRegistration")).isTrue();
}
}

Best Practices

  1. Environment Separation: Use different profiles for dev, staging, prod
  2. Sensitive Data Encryption: Always encrypt passwords and API keys
  3. Configuration Validation: Validate configuration on application startup
  4. Fail-Fast: Configure clients to fail fast if config server is unavailable
  5. Monitoring: Monitor config server health and configuration changes
  6. Version Control: Use Git tags for configuration versions
// Example of configuration validation
@Component
@Slf4j
public class ConfigValidator {
private final DynamicConfigService configService;
@EventListener(ApplicationReadyEvent.class)
public void validateConfiguration() {
log.info("Validating application configuration...");
try {
validateDatabaseConfig();
validateSecurityConfig();
validateExternalServices();
log.info("Configuration validation completed successfully");
} catch (Exception e) {
log.error("Configuration validation failed", e);
throw new IllegalStateException("Invalid configuration", e);
}
}
private void validateDatabaseConfig() {
DatabaseConfig dbConfig = configService.getDatabaseConfig();
if (dbConfig.getUrl() == null || dbConfig.getUrl().trim().isEmpty()) {
throw new ValidationException("Database URL must be configured");
}
}
private void validateSecurityConfig() {
SecurityConfig securityConfig = configService.getSecurityConfig();
if (securityConfig.getJwtSecret() == null || securityConfig.getJwtSecret().trim().isEmpty()) {
throw new ValidationException("JWT secret must be configured");
}
}
private void validateExternalServices() {
ExternalServiceConfig externalConfig = configService.getExternalServiceConfig();
if (externalConfig.getPaymentServiceUrl() == null) {
log.warn("Payment service URL is not configured");
}
}
}

Conclusion

Spring Cloud Config provides:

  • Centralized configuration management for distributed systems
  • Environment-specific configurations with profile support
  • Version control and audit trails through Git integration
  • Dynamic configuration updates without application restarts
  • Secure sensitive data with encryption support

This comprehensive implementation enables robust configuration management for Java applications, supporting both simple key-value configurations and complex structured configurations with dynamic refresh capabilities. The solution scales from small applications to large microservices architectures while maintaining security and reliability.

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