Configuration Management — Deep Dive
Level: Intermediate Pre-reading: 09 · Deployment & Infrastructure · 12-Factor App
Configuration vs Code
Configuration: Values that change between environments (dev, staging, prod). Code: Logic that stays the same.
| Item | Type | Where Stored |
|---|---|---|
| Database URL | Config | Environment variable / ConfigMap |
| Feature flag | Config | ConfigServer / Feature store |
| Price calculation logic | Code | Source code |
| API response format | Code | Source code |
| Kafka broker URL | Config | Environment variable |
| Log level | Config | Environment variable / ConfigMap |
The 12-Factor Rule: Store Config in Environment
Never bake configuration into code:
❌ Bad:
public class PaymentConfig {
public static final String DB_URL = "jdbc:mysql://prod-db:3306/payment";
public static final String API_KEY = "sk-1234567890"; // Secret in code!
}
✅ Good:
public class PaymentConfig {
public String dbUrl = System.getenv("DB_URL");
public String apiKey = System.getenv("API_KEY");
}
Benefits:
- Same jar/image for all environments
- Secrets not in Git history
- Change config without rebuild
Configuration Sources (Priority Order)
When reading a config value, check in order:
1. Command-line argument (highest priority)
2. Environment variable
3. Config file
4. Application defaults
5. System properties (lowest priority)
Example: Reading DB_URL
# Priority 1: Command-line
java -Dspring.datasource.url=jdbc:mysql://mydb:3306 app.jar
# Priority 2: Environment variable
export DB_URL=jdbc:mysql://mydb:3306
java app.jar
# Priority 3: application.yml
spring:
datasource:
url: jdbc:mysql://mydb:3306
# Priority 4: Default in code
@Value("${spring.datasource.url:jdbc:mysql://localhost:3306}")
private String dbUrl;
Spring Cloud Config Server
Centralized configuration management; change config without redeploying.
Architecture
graph LR
S["Service Pod<br/>Order Service"] -->|"GET /actuator/env"| CS["Config Server"]
CS -->|"Config values<br/>db.url, api.key, etc"| S
A["Admin"]
GIT["Git Repo<br/>config-repo/"]
A -->|"git push config"| GIT
GIT -->|"git pull"| CS
Setup
1. Config Server (central service)
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
# application.yml for Config Server
server:
port: 8888
spring:
cloud:
config:
server:
git:
uri: https://github.com/mycompany/config-repo
default-label: main
2. Git Repo Structure
config-repo/
├── application.yml (shared by all services)
├── order-service.yml (Order Service defaults)
├── order-service-dev.yml (Order Service + dev profile)
├── order-service-staging.yml (Order Service + staging profile)
├── order-service-prod.yml (Order Service + prod profile)
└── payment-service.yml
3. Config Client (in Order Service)
# bootstrap.yml (loads BEFORE application.yml)
spring:
application:
name: order-service
cloud:
config:
uri: http://config-server:8888
profile: ${ENVIRONMENT:dev} # dev, staging, prod
4. Client Reads Config
@RestController
public class OrderController {
@Value("${database.url}")
private String dbUrl;
@Value("${payment.api.key}")
private String apiKey;
@GetMapping("/config-check")
public Map<String, String> checkConfig() {
return Map.of(
"dbUrl", dbUrl,
"apiKey", apiKey
);
}
}
Environment-Specific Configuration
Profile-Based
# application.yml
spring:
profiles:
active: ${ENVIRONMENT:dev}
---
# application-dev.yml
spring:
profiles: dev
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
---
# application-prod.yml
spring:
profiles: prod
datasource:
url: jdbc:mysql://prod-rds:3306/payment
hikari:
maximum-pool-size: 50
Property Hierarchy
application.yml
↓
application-${ENVIRONMENT}.yml (overrides application.yml)
↓
Environment variables (highest priority)
Secrets Management
Never store secrets in ConfigServer or Git.
Option 1: External Secrets Manager
@Configuration
public class SecretsConfig {
@Bean
public String apiKey(@Value("${secrets.api-key}") String apiKey) {
// Read from AWS Secrets Manager, HashiCorp Vault, or Azure KeyVault
// via a secrets agent
return apiKey;
}
}
In Kubernetes, use an External Secrets Operator:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: payment-api-secret
spec:
secretStoreRef:
name: aws-secrets
target:
name: payment-secret # Create K8s Secret with this name
template:
type: Opaque
data:
- secretKey: api-key
remoteRef:
key: payment-service/api-key
Then in application:
@Configuration
public class ApiKeyConfig {
@Bean
public String apiKey() {
// Kubernetes mounts secret as file or env var
return System.getenv("PAYMENT_API_KEY");
}
}
Option 2: Kubernetes Secrets (Encrypted)
apiVersion: v1
kind: Secret
metadata:
name: payment-secrets
type: Opaque
data:
api-key: c2stMTIzNDU2Nzg5MA== # base64 encoded
db-password: cGFzc3dvcmQxMjM=
apiVersion: v1
kind: Pod
spec:
containers:
- name: payment-service
env:
- name: PAYMENT_API_KEY
valueFrom:
secretKeyRef:
name: payment-secrets
key: api-key
Feature Flags (Runtime Configuration)
Change behavior without redeploying.
Manual Implementation
@RestController
public class OrderController {
@Autowired
private FeatureFlagService featureFlagService;
@PostMapping("/orders")
public Order createOrder(@RequestBody OrderRequest request) {
Order order = buildOrder(request);
// Feature flag: conditionally use new payment processor
if (featureFlagService.isEnabled("use-new-payment-processor")) {
order = PaymentService.processWithNewProvider(order);
} else {
order = PaymentService.processWithLegacyProvider(order);
}
return order;
}
}
@Service
public class FeatureFlagService {
@Autowired
private ConfigServer configServer;
public boolean isEnabled(String flagName) {
return configServer.getBoolean("feature.flags." + flagName);
}
}
External Service (LaunchDarkly, Split.io)
// Evaluate flag with context (user ID, region, etc)
LDUser user = new LDUser.Builder("user-123")
.country("US")
.build();
boolean enabled = ldClient.boolVariation(
"payment-processor-v2",
user,
false // default
);
if (enabled) {
// Use new payment processor for this user
}
Benefits of external service:
- Gradual rollout (10% users → 50% → 100%)
- A/B testing (variant A vs variant B)
- Kill switch (disable instantly without redeployment)
Configuration Best Practices
| Practice | Benefit |
|---|---|
| Use environment variables for secrets | Secrets not in code/Git |
| Store non-secret config in ConfigServer | Changes without redeployment |
| Use profiles for environment-specific values | Same code, different deployments |
| Document all config values | Team knows what's available |
| Validate config on startup | Fail fast; not in production |
| Use feature flags for risky changes | Gradual rollout; easy rollback |
| Version config like code | History; audits; rollback if needed |
Should I use ConfigServer or environment variables?
Both: environment variables for secrets (API keys, passwords); ConfigServer for non-secret config that changes often (log levels, feature flags, URLs). This separates concerns and keeps secrets secure.
How do I reload config without restarting the service?
Spring Cloud ConfigClient supports @RefreshScope annotation. Change config in Git; POST to /actuator/refresh endpoint; client reloads. Be careful: some beans don't support refresh; test in staging first.
Can I store secrets in ConfigServer with encryption?
Yes, Spring Cloud ConfigServer supports encrypted values. But it's better to use external secrets manager (Vault, AWS Secrets Manager) for production. ConfigServer is for non-sensitive config.