Java Notes
Centralized configuration management with Spring Cloud Config Server - externalizing configuration, Git-based config, refresh scope, and encrypted properties.
Introduction
In a microservices architecture, each service has its own configuration (database URLs, API keys, feature flags, timeouts). Managing these configurations across dozens of services and multiple environments (dev, staging, production) becomes a nightmare. Spring Cloud Config Server provides a centralized, externalized configuration management solution.
The Problem
Without centralized config:
- Configuration is scattered across multiple
application.ymlfiles - Changing a property requires rebuilding and redeploying the service
- No audit trail of configuration changes
- Secrets are embedded in source code
- Environment-specific configs are hardcoded
The Solution
Spring Cloud Config Server:
- Single source of truth for all service configurations
- Git-backed storage with version history and audit trail
- Environment-specific configs (dev, staging, prod) without code changes
- Dynamic refresh without restarting services
- Encryption/Decryption for sensitive properties
- Multi-format support — YAML, Properties, JSON
Setting Up the Config Server
Step 1: Create the Config Server Project
pom.xml (Maven Dependencies):
Step 2: Main Application Class
package com.wohotech.configserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
/**
* @EnableConfigServer — This single annotation transforms this Spring Boot
* application into a Config Server. It:
* 1. Exposes REST endpoints for config retrieval
* 2. Connects to configured backend (Git, file system, Vault)
* 3. Serves configurations in multiple formats (JSON, YAML, Properties)
* 4. Supports environment-specific and label-specific configs
*/
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}Step 3: Config Server Configuration
application.yml (Config Server's own configuration):
server:
port: 8888 # Standard port for config servers
spring:
application:
name: config-server
cloud:
config:
server:
git:
# Remote Git repository containing all service configs
uri: https://github.com/wohotech/microservices-config.git
# Branch to use (default: main)
default-label: main
# Directory search paths within the repo
search-paths:
- '{application}' # Look in folder named after the service
- shared # Shared configurations
# Clone on startup (fail fast if git is unreachable)
clone-on-start: true
# For private repositories
username: ${GIT_USERNAME:}
password: ${GIT_PASSWORD:}
# Timeout settings
timeout: 5
# Force pull to overwrite local changes
force-pull: true
# Encryption key for sensitive properties
encrypt:
enabled: true
# Security credentials for clients to access config server
security:
user:
name: configuser
password: ${CONFIG_SERVER_PASSWORD:configpass123}
# Actuator endpoints
management:
endpoints:
web:
exposure:
include: health, info, bus-refresh, bus-env
endpoint:
health:
show-details: always
# Encryption key (symmetric)
encrypt:
key: ${ENCRYPT_KEY:my-secret-encryption-key-change-in-production}
# Logging
logging:
level:
org.springframework.cloud.config: DEBUGSetting Up the Git Repository
Repository Structure
Create a Git repository with this structure:
Example Config Files
application.yml (Shared — applies to ALL services):
# Common settings for all microservices
spring:
jackson:
serialization:
write-dates-as-timestamps: false
default-property-inclusion: non_null
management:
endpoints:
web:
exposure:
include: health, info, metrics, refresh
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
logging:
level:
root: INFO
com.wohotech: DEBUGuser-service/user-service.yml (Default profile):
spring:
datasource:
url: jdbc:mysql://localhost:3306/userdb
username: user_service
password: '{cipher}AQB+7hKJx4Q2...' # Encrypted value
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: false
server:
port: 8081
app:
jwt:
secret: '{cipher}AQA6N7hKJx4Q2...'
expiration: 86400000
email:
from: noreply@wohotech.com
max-login-attempts: 5
account-lock-duration: 30 # minutes
# Custom message for testing
welcome:
message: "Welcome to User Service - Default Profile"user-service/user-service-dev.yml (Dev profile overrides):
spring:
datasource:
url: jdbc:mysql://dev-db.wohotech.internal:3306/userdb_dev
username: dev_user
password: '{cipher}AQC+8sLMN9...'
jpa:
show-sql: true
hibernate:
ddl-auto: create-drop # Recreate schema in dev
server:
port: 0 # Random port in dev for multiple instances
logging:
level:
com.wohotech: TRACE
org.hibernate.SQL: DEBUG
welcome:
message: "Welcome to User Service - DEV Environment"user-service/user-service-prod.yml (Production overrides):
spring:
datasource:
url: jdbc:mysql://prod-cluster.wohotech.cloud:3306/userdb_prod
username: prod_user
password: '{cipher}AQD+1xPQR2...'
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 20000
jpa:
show-sql: false
hibernate:
ddl-auto: validate # Never auto-modify prod schema
server:
port: 8081
logging:
level:
root: WARN
com.wohotech: INFO
welcome:
message: "Welcome to User Service - PRODUCTION"Client-Side Configuration
Step 1: Add Dependencies to Client Service
Step 2: Client Application Configuration
application.yml (in user-service):
spring:
application:
name: user-service # This name is used to fetch config
# Config Server connection
config:
import: optional:configserver:http://localhost:8888
cloud:
config:
# Credentials to access config server
username: configuser
password: configpass123
# Fail fast if config server is unavailable
fail-fast: true
# Retry settings
retry:
initial-interval: 1000
max-interval: 5000
max-attempts: 6
multiplier: 1.5
# Profile to activate
profile: dev # Can be overridden by SPRING_PROFILES_ACTIVE env var
# Git label (branch)
label: mainImportant: In Spring Boot 3.x / Spring Cloud 2022+, we usespring.config.importinstead of the deprecatedbootstrap.ymlapproach.
Step 3: Using Configuration Values in Code
package com.wohotech.userservice.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* @RefreshScope — Makes this bean re-instantiated when /actuator/refresh is called.
* Without @RefreshScope, @Value-injected properties remain stale after config change.
*
* How it works:
* 1. Bean is marked as "lazy proxy"
* 2. On /actuator/refresh call, the bean is destroyed
* 3. Next request recreates the bean with updated config values
* 4. Only beans with @RefreshScope are affected
*/
@RestController
@RequestMapping("/api/v1/config")
@RefreshScope
public class ConfigTestController {
@Value("${welcome.message:Default Welcome}")
private String welcomeMessage;
@Value("${app.max-login-attempts:3}")
private int maxLoginAttempts;
@Value("${app.jwt.expiration:3600000}")
private long jwtExpiration;
@GetMapping("/info")
public Map<String, Object> getConfigInfo() {
return Map.of(
"welcomeMessage", welcomeMessage,
"maxLoginAttempts", maxLoginAttempts,
"jwtExpiration", jwtExpiration
);
}
}Step 4: Configuration Properties Class (Type-Safe)
package com.wohotech.userservice.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
/**
* Type-safe configuration binding.
* All properties under "app" prefix are mapped to this class.
*
* In application.yml:
* app:
* jwt:
* secret: my-secret
* expiration: 86400000
* max-login-attempts: 5
* account-lock-duration: 30
*/
@Component
@ConfigurationProperties(prefix = "app")
@RefreshScope
@Getter @Setter
public class AppProperties {
private JwtProperties jwt = new JwtProperties();
private int maxLoginAttempts = 3;
private int accountLockDuration = 30;
private EmailProperties email = new EmailProperties();
@Getter @Setter
public static class JwtProperties {
private String secret;
private long expiration = 86400000; // 24 hours
}
@Getter @Setter
public static class EmailProperties {
private String from = "noreply@example.com";
}
}@RefreshScope — Dynamic Configuration Updates
How Refresh Works
When configuration changes in Git:
- Manual Refresh (Single Service):
# Trigger refresh on a specific service instance
curl -X POST http://user-service:8081/actuator/refresh -H "Content-Type: application/json"Response:
(Returns list of changed property keys)
- Broadcast Refresh (All Services) — Spring Cloud Bus:
# Trigger refresh on ALL connected services via message bus
curl -X POST http://config-server:8888/actuator/bus-refreshThis sends a refresh event via RabbitMQ/Kafka to all client services.
What Gets Refreshed
- Beans annotated with
@RefreshScope @ConfigurationPropertiesbeans with@RefreshScope@Value-injected fields in@RefreshScopebeans
What Does NOT Get Refreshed
- DataSource connections (need Spring Cloud's
@RefreshScopeon DataSource bean) - Static final fields
- Beans without
@RefreshScope - Constructor-injected values (already set at creation time)
Spring Cloud Bus Setup
# In config server and all client services
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
cloud:
bus:
enabled: true
refresh:
enabled: trueFlow:
Encryption and Decryption
Symmetric Encryption
Configure encryption key:
# In config server application.yml
encrypt:
key: ${ENCRYPT_KEY:s3cr3t-k3y-f0r-3ncrypt10n}Encrypt a value:
# Encrypt a password using config server's endpoint
curl -X POST http://localhost:8888/encrypt -H "Content-Type: text/plain" -d "myDatabasePassword123"
# Response: AQB+7hKJx4Q2N6xG9LwJD7hQfN0vz...Use encrypted value in config:
spring:
datasource:
password: '{cipher}AQB+7hKJx4Q2N6xG9LwJD7hQfN0vz...'Decrypt a value (for verification):
curl -X POST http://localhost:8888/decrypt -H "Content-Type: text/plain" -d "AQB+7hKJx4Q2N6xG9LwJD7hQfN0vz..."
# Response: myDatabasePassword123Asymmetric Encryption (RSA — More Secure)
Step 1: Generate keystore:
keytool -genkeypair -alias config-server-key -keyalg RSA -keysize 2048 -dname "CN=Config Server,OU=WohoTech,O=WohoTech,L=Bangalore,S=KA,C=IN" -keypass changeme -keystore config-server.jks -storepass changeme -validity 365Step 2: Configure in application.yml:
encrypt:
key-store:
location: classpath:config-server.jks
password: changeme
alias: config-server-key
secret: changemeStep 3: The server now encrypts/decrypts using RSA keypair:
# Same encrypt/decrypt endpoints work
curl -X POST http://localhost:8888/encrypt -d "secret-value"Best Practice: In production, use HashiCorp Vault or AWS KMS instead of storing keys in config.
Advanced Configuration Patterns
Pattern 1: Config Server with Multiple Backends
spring:
cloud:
config:
server:
git:
uri: https://github.com/wohotech/microservices-config.git
repos:
# Specific repo for payment service (higher security)
payment:
pattern: payment-service*
uri: https://github.com/wohotech/payment-config-private.git
username: ${PAYMENT_GIT_USER}
password: ${PAYMENT_GIT_TOKEN}
# Team-specific configs
team-alpha:
pattern: alpha-*
uri: https://github.com/wohotech/team-alpha-config.gitPattern 2: Config Server with Vault Backend
spring:
cloud:
config:
server:
vault:
host: vault.wohotech.internal
port: 8200
scheme: https
authentication: TOKEN
token: ${VAULT_TOKEN}
kv-version: 2
default-key: application
# Composite: Git for non-secrets, Vault for secrets
composite:
- type: git
uri: https://github.com/wohotech/microservices-config.git
- type: vaultPattern 3: Native File System Backend (for local development)
spring:
profiles:
active: native # Use local file system instead of Git
cloud:
config:
server:
native:
search-locations:
- file:///C:/config-repo
- classpath:/configsPattern 4: Health Indicator Configuration
package com.wohotech.configserver.config;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.cloud.config.server.environment.EnvironmentRepository;
import org.springframework.stereotype.Component;
@Component
public class ConfigServerHealthIndicator implements HealthIndicator {
private final EnvironmentRepository environmentRepository;
public ConfigServerHealthIndicator(EnvironmentRepository environmentRepository) {
this.environmentRepository = environmentRepository;
}
@Override
public Health health() {
try {
// Try to fetch a known config to verify Git connectivity
environmentRepository.findOne("application", "default", "main");
return Health.up()
.withDetail("git", "connected")
.withDetail("status", "Config repository accessible")
.build();
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}Config Server REST API
The config server exposes these endpoints automatically:
| Endpoint Pattern | Description | Example |
|---|---|---|
GET /{application}/{profile} | Config for app + profile | /user-service/dev |
GET /{application}/{profile}/{label} | Config for app + profile + branch | /user-service/dev/main |
GET /{application}-{profile}.yml | YAML format | /user-service-dev.yml |
GET /{application}-{profile}.properties | Properties format | /user-service-dev.properties |
GET /{application}-{profile}.json | JSON format | /user-service-dev.json |
POST /encrypt | Encrypt a plain text value | Body: my-secret |
POST /decrypt | Decrypt a cipher text | Body: AQB+7hK... |
POST /actuator/bus-refresh | Refresh all clients | - |
Example API Responses
GET http://localhost:8888/user-service/dev
Resolution Order (Priority high → low): 1.user-service-dev.yml(application + profile) 2.user-service.yml(application default) 3.application-dev.yml(shared + profile) 4.application.yml(shared default)
Security Configuration
Securing the Config Server
package com.wohotech.configserver.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/encrypt/**", "/decrypt/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(httpBasic -> {}); // Basic auth for simplicity
return http.build();
}
}Client Authentication to Config Server
# In client service
spring:
cloud:
config:
username: configuser
password: ${CONFIG_SERVER_PASSWORD}
# OR use token-based:
# token: ${CONFIG_TOKEN}Docker Deployment
Dockerfile for Config Server
Docker Compose
Complete Workflow: Git Webhook → Auto Refresh
Set Up GitHub Webhook
- Go to your config repo → Settings → Webhooks
- Payload URL:
https://your-config-server.com/monitor - Content type:
application/json - Events: Push events only
Config Server Webhook Endpoint
# Add monitor endpoint support
spring:
cloud:
config:
server:
monitor:
github:
enabled: trueWhen a push happens:
Common Issues & Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Client can't connect to config server | Wrong URL or port | Check spring.config.import URL |
| Config values are null | Application name mismatch | Ensure spring.application.name matches config file name |
| Encrypted values not decrypted | Missing encryption key | Set encrypt.key in config server |
| Changes not reflected | Missing @RefreshScope | Add @RefreshScope to beans using @Value |
| Config server 401 | Wrong credentials | Check username/password in client config |
| Git clone fails | Private repo without credentials | Configure git.username and git.password |
| Stale config after git push | No refresh triggered | Call /actuator/refresh or set up bus-refresh |
Interview Questions
Q1: What is Spring Cloud Config Server and why is it needed?
Answer: Spring Cloud Config Server provides centralized externalized configuration management for distributed systems. In microservices, each service may run multiple instances across environments. Without centralized config: (1) Configuration is duplicated, (2) Changing a value requires redeployment, (3) Secrets end up in source code, (4) No audit trail. Config Server solves this by storing all configs in a Git repo (version-controlled), serving them via REST API, and supporting dynamic refresh without restart.
Q2: How does the Config Server resolve configurations for a specific service?
Answer: When a client with name "user-service" and profile "dev" on branch "main" requests config, the server checks files in this order (highest priority first): user-service-dev.yml → user-service.yml → application-dev.yml → application.yml. Properties from higher-priority sources override lower ones. The {label} parameter maps to a Git branch, allowing different configs per branch (feature branches, release branches).
Q3: What is @RefreshScope and how does it work?
Answer: @RefreshScope is a Spring Cloud annotation that creates beans as lazy proxies. When /actuator/refresh is called: (1) The ApplicationContext publishes a RefreshScopeRefreshedEvent, (2) All @RefreshScope beans are destroyed (their cache is cleared), (3) On next access, the bean is recreated with fresh configuration values. Without @RefreshScope, @Value injections happen at bean creation time and never update. Internal implementation uses a custom scope that wraps beans in a GenericScope cache.
Q4: How do you handle sensitive properties (passwords, API keys)?
Answer: Three approaches: (1) Encryption — Use config server's /encrypt endpoint with symmetric (AES) or asymmetric (RSA) keys. Store as '{cipher}encrypted-value'. Server decrypts before sending to clients. (2) Environment variables — Reference with ${ENV_VAR} in configs, inject at deployment time. (3) Vault integration — Spring Cloud Config supports HashiCorp Vault as a backend. Secrets are stored in Vault with TTL and access policies. Best practice: Use Vault for production secrets, encryption for less critical values.
Q5: What is Spring Cloud Bus and how does it integrate with Config Server?
Answer: Spring Cloud Bus links microservice instances via a lightweight message broker (RabbitMQ/Kafka). When config changes: (1) You call /actuator/bus-refresh on the config server, (2) It publishes a refresh event to the message bus, (3) ALL subscribed service instances receive the event, (4) Each instance calls its own refresh logic (re-fetching from config server). Without Bus, you'd need to call /refresh on every individual instance separately. It enables one-click refresh across the entire microservices fleet.
Q6: What happens if the Config Server is down when a client starts up?
Answer: By default with spring.cloud.config.fail-fast=true, the client fails to start (fast failure). With fail-fast=false, the client starts with local/default values. Best practices: (1) Enable retry with exponential backoff (spring.cloud.config.retry.*), (2) Run config server with high availability (multiple instances behind load balancer), (3) Use config server's clone-on-start to have a local cache, (4) Client-side caching — once config is fetched, it's kept in memory even if config server goes down later.
Q7: How would you implement Config Server high availability?
Answer: (1) Run 2-3 config server instances behind a load balancer, (2) All instances point to the same Git repo, (3) Use Spring Cloud Discovery (Eureka) so clients find config server via service registry, (4) Enable Git clone-on-start so each instance has a local clone (survives Git outage), (5) Use force-pull: true to always fetch latest, (6) For stateless HA, avoid using the native file backend (use Git/Vault), (7) Health checks on /actuator/health for load balancer routing.
Key Annotations Reference
| Annotation | Location | Purpose |
|---|---|---|
@EnableConfigServer | Server main class | Activates config server functionality |
@RefreshScope | Client beans | Enables dynamic refresh of bean |
@ConfigurationProperties | Client config class | Type-safe config binding |
@Value("${key}") | Client field | Inject single property value |
Summary: 12-Factor App Configuration
The Config Server implements the 12-Factor App methodology:
- Store config in the environment — Not in code, in externalized config
- Strict separation — Config varies between deploys, code does not
- Config is the only thing that changes between environments
By using Spring Cloud Config Server:
- ✅ Configuration is externalized (Git/Vault)
- ✅ Environment-specific (profiles: dev, staging, prod)
- ✅ Version-controlled (Git commit history)
- ✅ Dynamically updatable (refresh without restart)
- ✅ Secure (encryption for secrets)
- ✅ Centrally managed (single source of truth)
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Config Server.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Java Master Course topic.
Search Terms
java-master-course, java master course, java, master, course, spring, framework, microservices
Related Java Master Course Topics