07-spring-cloud

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.

October 9, 2026
spring-cloudconfig-serverconfigurationsecretskubernetesconfigmapvaultrefresh-scope

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

java
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}
yaml
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:

text
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}:

bash
curl http://localhost:8888/orders/prod
curl http://localhost:8888/orders/prod/main

Spring 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

groovy
dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-config'
}
yaml
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.5

Configuration 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
1Command-line arguments
2Environment variables
3Config Server
4Local application-{profile}.yml
5Local 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.

yaml
spring:
  cloud:
    config:
      override-system-properties: false    # keep env vars winning (the default)
      allow-override: true

The 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.

java
@Component
@RefreshScope                       // recreated on refresh
public class FeatureFlags {
 
    @Value("${features.new-checkout:false}")
    private boolean newCheckout;
 
    public boolean isNewCheckoutEnabled() {
        return newCheckout;
    }
}
bash
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:

groovy
dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp'
}
bash
curl -X POST http://config-server:8888/actuator/busrefresh
⚠️

Refresh 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:

bash
curl http://config-server:8888/encrypt -d 'my-database-password'
# → AQA7a9Hk2...
yaml
spring:
  datasource:
    password: '{cipher}AQA7a9Hk2...'

The server decrypts before serving. The key lives in the server's keystore:

yaml
encrypt:
  key-store:
    location: file:/etc/config-server/keystore.jks
    password: ${KEYSTORE_PASSWORD}
    alias: config-key
🚨

This 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:

yaml
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 ServerKubernetes ConfigMapSecrets manager
Change auditGit historyLimitedFull audit log
Review workflowPull requestkubectl / GitOpsVaries
Refresh without restartYes (Bus)Pod restart, or mounted-file reloadVaries
Secret rotationNoNoYes
Per-secret access controlNoRBAC by namespaceYes
Extra service to operateYesNoManaged
Works outside KubernetesYesNoYes

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

  1. Centralisation solves duplication and drift across services, not configuration in general.
  2. On Kubernetes, ConfigMaps may be enough. Two config sources is worse than one.
  3. Config Server is a startup dependency — replicate it, use fail-fast with retry, keep baked-in defaults.
  4. Precedence: env vars > Config Server > local files.
  5. Refresh works for values, not structure. Pool sizes and ports stay fixed.
  6. {cipher} values are not a secrets manager — no rotation, no audit, ciphertext in Git forever.
  7. Authenticate the Config Server.
  8. 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.