Centralised Config: Config Server or ConfigMaps
Git-backed configuration served over HTTP, refresh without restart, encrypted values — and an honest comparison with what your platform already offers.
The Problem at Twelve Services
Phase 1 established the rule: one artefact, configuration from the environment. That works well for one service.
At twelve services across four environments it strains. The same database host appears in six application-prod.yml files. Changing a shared timeout means twelve pull requests. Nobody can answer "what is the current configuration of everything in staging" without reading twelve repositories. And a value changed in one place and not another produces drift that only shows up as a bug.
Centralised configuration puts it in one place. Spring Cloud Config Server is one way; your platform probably offers another.
Read This First
On Kubernetes, ConfigMaps and Secrets already do most of this, and Spring Cloud Kubernetes can bind them directly as property sources. Running Config Server alongside means another service to operate, another startup dependency, and two places configuration can live.
Config Server earns its place when you are not on a platform with native configuration, when you want Git as the audit trail with pull-request review for config changes, or when you need one config source across several platforms (some VMs, some Kubernetes). For a greenfield service on Kubernetes, ConfigMaps plus a secrets manager is usually less machinery.
The concepts are worth knowing either way — precedence, refresh and secret handling are the same problems regardless of mechanism.
Config Server
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}server:
port: 8888
spring:
cloud:
config:
server:
git:
uri: https://github.com/example/shop-config
default-label: main
clone-on-start: true
search-paths: '{application}'The Git repository holds the files:
shop-config/
├── application.yml # shared by every service
├── application-prod.yml # shared, prod only
├── orders.yml # the orders service
├── orders-prod.yml # orders, prod only
└── inventory.yml
Served over HTTP at /{application}/{profile}/{label}:
curl http://localhost:8888/orders/prod
curl http://localhost:8888/orders/prod/mainSpring resolves in order of increasing specificity — application.yml, then application-prod.yml, then orders.yml, then orders-prod.yml — with the most specific winning. The same layering as profile-specific files, applied across services.
The client
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-config'
}spring:
application:
name: orders # determines which files apply
config:
import: "optional:configserver:http://config-server:8888"
cloud:
config:
fail-fast: true
retry:
max-attempts: 6
initial-interval: 1000
multiplier: 1.5Configuration is fetched before the ApplicationContext is created, so values are available to everything including auto-configuration.
Config Server becomes a startup dependency for every service. If it's down during a deployment or a scale-up, services can't start — and that is exactly when you need them to.
Three mitigations, and use all of them. fail-fast: true with retry, so a brief blip is survived rather than crashing. Run at least two replicas. And keep a baked-in application.yml with safe defaults so a service can start degraded rather than not at all. The optional: prefix above permits that; dropping it makes Config Server strictly required.
Precedence
Config Server adds a layer to phase 1's precedence order. Highest priority first:
| # | Source |
|---|---|
| 1 | Command-line arguments |
| 2 | Environment variables |
| 3 | Config Server |
| 4 | Local application-{profile}.yml |
| 5 | Local application.yml |
Note where Config Server sits: above local files, below environment variables. Which gives a useful property — centralised configuration overrides a service's own defaults, while an environment variable can still override Config Server for a one-off or an emergency.
spring:
cloud:
config:
override-system-properties: false # keep env vars winning (the default)
allow-override: trueThe roadmap states "local > Config Server > defaults", which is right in that local environment takes precedence; local files do not.
Refresh Without Restart
Config Server's distinctive capability: change a value and have running instances pick it up.
@Component
@RefreshScope // recreated on refresh
public class FeatureFlags {
@Value("${features.new-checkout:false}")
private boolean newCheckout;
public boolean isNewCheckoutEnabled() {
return newCheckout;
}
}curl -X POST http://orders:9001/actuator/refresh@RefreshScope beans are destroyed and recreated on next access with the new values. @ConfigurationProperties beans are rebound without needing the annotation.
To refresh every instance at once, Spring Cloud Bus broadcasts over RabbitMQ or Kafka:
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp'
}curl -X POST http://config-server:8888/actuator/busrefreshRefresh is narrower than it looks. It does not re-create the ApplicationContext, so anything fixed at startup stays fixed: the Hikari pool size, the embedded server's port, @Bean definitions, and conditional bean existence. Changing server.port by refresh does nothing.
It works well for values read through @RefreshScope or @ConfigurationProperties — feature flags, thresholds, timeouts, log levels. Treat it as a tool for tuning knobs, not for restructuring an application. And test that a refresh does what you expect: a bean caching a value in a field at construction won't see the change even inside @RefreshScope unless it's recreated.
Secrets
Config Server can encrypt values:
curl http://config-server:8888/encrypt -d 'my-database-password'
# → AQA7a9Hk2...spring:
datasource:
password: '{cipher}AQA7a9Hk2...'The server decrypts before serving. The key lives in the server's keystore:
encrypt:
key-store:
location: file:/etc/config-server/keystore.jks
password: ${KEYSTORE_PASSWORD}
alias: config-keyThis is weaker than a real secrets manager, and worth being clear about why. The ciphertext sits in Git forever — so a future key compromise exposes historical secrets, and a git log of the config repo is a target. There is no rotation support, no access audit per secret, no automatic expiry, and no per-service access control: any service that can reach Config Server can read any configuration it serves.
For a non-production database password it's a reasonable convenience. For production credentials, use a secrets manager — HashiCorp Vault (which Config Server can use as a backend), AWS Secrets Manager, GCP Secret Manager, or Kubernetes Secrets with an external-secrets operator. Those give rotation, audit and scoped access, which encrypted-values-in-Git fundamentally cannot.
Secure the server itself regardless:
spring:
security:
user:
name: config
password: ${CONFIG_SERVER_PASSWORD}An unauthenticated Config Server serves your entire system's configuration to anyone who can reach it.
Comparing the Options
| Config Server | Kubernetes ConfigMap | Secrets manager | |
|---|---|---|---|
| Change audit | Git history | Limited | Full audit log |
| Review workflow | Pull request | kubectl / GitOps | Varies |
| Refresh without restart | Yes (Bus) | Pod restart, or mounted-file reload | Varies |
| Secret rotation | No | No | Yes |
| Per-secret access control | No | RBAC by namespace | Yes |
| Extra service to operate | Yes | No | Managed |
| Works outside Kubernetes | Yes | No | Yes |
A combination is common and sensible: ConfigMaps or Config Server for non-secret configuration, a secrets manager for secrets. The distinction is whether exposure of a value is merely untidy or an incident.
Check yourself
A team stores production database passwords as {cipher} values in the Config Server Git repository. A developer who left six months ago still has a clone of that repo. What is the exposure?
What to Centralise
Not everything benefits:
Centralise — values shared across services (a shared database host, a common timeout), environment-specific endpoints, feature flags, log levels, and thresholds you tune operationally.
Keep local — anything specific to one service that only changes with its code. A service's own bean wiring and its internal defaults belong in its repository, where they're reviewed alongside the code that uses them.
Centralising a service's every property makes its behaviour unreadable from its own codebase — a reader has to consult another repository to know what the service does. That's a real cost, and it's why "central for shared, local for specific" is the better line than "central for everything".
The Mental Model, Restated
- Centralisation solves duplication and drift across services, not configuration in general.
- On Kubernetes, ConfigMaps may be enough. Two config sources is worse than one.
- Config Server is a startup dependency — replicate it, use
fail-fastwith retry, keep baked-in defaults. - Precedence: env vars > Config Server > local files.
- Refresh works for values, not structure. Pool sizes and ports stay fixed.
{cipher}values are not a secrets manager — no rotation, no audit, ciphertext in Git forever.- Authenticate the Config Server.
- Central for shared, local for service-specific.
What's Next
One guide remains. With services discovered, routed, balanced, protected and configured, the final question is how you debug a request that touched six of them. The last guide covers distributed tracing end to end — context propagation, sampling, and the correlation that makes a multi-service system diagnosable.