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
- Environment Separation: Use different profiles for dev, staging, prod
- Sensitive Data Encryption: Always encrypt passwords and API keys
- Configuration Validation: Validate configuration on application startup
- Fail-Fast: Configure clients to fail fast if config server is unavailable
- Monitoring: Monitor config server health and configuration changes
- 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.