Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

Spring Cloud Config & Centralised Configuration

Centralised configuration done safely: Config Server with a Git backend, client bootstrap and fail-fast, @RefreshScope, encrypted values, and when Kubernetes ConfigMaps are enough.

Intermediate5 min readUpdated
On this page

Centralised configuration solves a real problem: fifteen services each with their own copy of a database URL, and a rotation that requires fifteen deploys. It also introduces a dependency every service needs at startup, which has to be designed for.

It is worth asking up front whether you need it at all. On Kubernetes, ConfigMaps and Secrets already give you per-environment configuration delivered by the platform, versioned in the same Git repository as the manifests, with no extra service to run or keep available. Spring Cloud Config earns its place when you are not on such a platform, when configuration must change without a pod restart, or when a single change needs to reach many services at once — not simply because the estate has become distributed.

Key Takeaways

  • Git as the backend gives configuration history, review and rollback for free.
  • Enable fail-fast plus retry — starting with missing config is worse than not starting.
  • @RefreshScope only suits small stateless beans; structural changes need a restart.
  • Spring Cloud Bus broadcasts a refresh to every instance instead of curling each one.
  • Encrypt secrets at rest with {cipher}, or better, keep them in Vault.

Server

ConfigServerApplication.java
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}
config-server application.yml
server:
  port: 8888
spring:
  cloud:
    config:
      server:
        git:
          uri: git@github.com:acme/service-config.git
          default-label: main
          clone-on-start: true          # fail at startup, not on first request
          force-pull: true              # discard local drift
          search-paths: '{application}' # a directory per service
          timeout: 10
          private-key: ${CONFIG_REPO_SSH_KEY}
        # Composite backend: Vault for secrets, Git for everything else.
        vault:
          host: vault.internal
          port: 8200
          scheme: https
          backend: secret
          order: 1
encrypt:
  key-store:
    location: file:/etc/config-server/keystore.jks
    password: ${KEYSTORE_PASSWORD}
    alias: config-key

The repository layout maps directly onto the resolution rules:

service-config/
├── application.yml              # defaults for every service
├── application-prod.yml         # prod defaults for every service
├── order-service/
│   ├── order-service.yml        # service defaults
│   └── order-service-prod.yml   # service + profile — highest precedence
└── payment-service/
    └── payment-service.yml

Properties merge from least to most specific, so application.yml provides the base and order-service-prod.yml overrides only what differs. This is the main advantage over per-service files: a change to a shared value happens once, is reviewed once, and applies everywhere.

Client

client application.yml
spring:
  application:
    name: order-service          # selects the directory in the repo
  config:
    import: 'configserver:http://config-server:8888'
  cloud:
    config:
      # Refuse to start without configuration rather than booting with defaults
      # that silently point at the wrong database.
      fail-fast: true
      retry:
        initial-interval: 1000
        max-interval: 5000
        max-attempts: 6
      label: main
      profile: ${SPRING_PROFILES_ACTIVE:default}

Note spring.config.import rather than the old bootstrap.yml. Since Spring Boot 2.4 the bootstrap context is legacy, and the import syntax is the supported path. Leave off the optional: prefix in production so a missing Config Server is an error rather than a silent fallback to packaged defaults — which is exactly the failure that sends a service at a development database.

Refreshing without a restart

Spring Cloud Bus turns one webhook into a refresh across every instance, rather than curling each pod individually.
RefreshableFeatureFlags.java
@Component
@RefreshScope
@ConfigurationProperties(prefix = "app.features")
public class FeatureFlags {
    private boolean newCheckoutFlow;
    private int maxBasketSize;
    // getters and setters
}

@RefreshScope works by replacing the bean with a proxy that recreates the target after a refresh event. That is fine for a small stateless holder of values and actively harmful for anything holding a resource — a DataSource, a Kafka consumer, a thread pool. Recreating those mid-flight drops connections and loses in-progress work.

The practical rule: refresh behavioural configuration such as flags, thresholds and timeouts; restart for structural configuration such as connection URLs, pool sizes and topic names. A rolling restart is cheap and predictable; a half-refreshed connection pool is neither.

Encryption

terminal
curl -s config-server:8888/encrypt -d 'my-database-password'
# AQBc9nJt4kL...
order-service.yml
spring:
  datasource:
    password: '{cipher}AQBc9nJt4kL...'

The client receives the decrypted value transparently, so secrets stay out of plaintext in Git while keeping review and history.

It is still weaker than a secret manager. The value is decrypted by Config Server, so anyone who can read from Config Server can read every secret; rotation means re-encrypting and committing; and there is no audit trail of who read what. For anything genuinely sensitive, use the Vault backend — dynamic database credentials with short leases mean a leaked credential expires on its own.

Do you actually need it?

On Kubernetes, ConfigMaps and Secrets already externalise configuration, the platform distributes them, and spring-cloud-kubernetes can watch a ConfigMap and trigger a refresh. For a single-cluster deployment that is usually enough, and it is one less service to run, secure and page someone about.

Config Server earns its cost in three situations: when you want configuration reviewed and versioned in Git with the same process as code; when you deploy across several platforms or clouds that do not share a configuration mechanism; or when you need dynamic refresh with a clear audit trail of who changed what and when.

Whichever you choose, treat the configuration source as critical infrastructure. It is a startup dependency for every service, so it needs replicas, monitoring, and a restore path you have actually tested — a Config Server whose Git credentials expired at 3am is an outage for the entire estate.

What to take away

Back Config Server with Git for history and review, and Vault for secrets. Turn on fail-fast with retry so services never boot half-configured. Use @RefreshScope narrowly and restart for structural change. And if you are on one Kubernetes cluster, check whether ConfigMaps already do what you need before adding another service to operate.

Frequently Asked Questions

Do I need Config Server on Kubernetes?
Often not. ConfigMaps and Secrets already externalise configuration, and the platform handles distribution. Config Server earns its place when you want configuration versioned in Git with review and history, when you run across several platforms, or when you need dynamic refresh without a pod restart.
What happens if Config Server is down when a service starts?
With fail-fast enabled the service refuses to start, which is correct — starting with missing configuration is worse than not starting. Add retry with backoff so a brief unavailability during a rolling restart does not cascade, and treat Config Server as critical infrastructure with replicas.
Is @RefreshScope safe to use everywhere?
No. A refresh-scoped bean is recreated on the next access, so anything holding state or an open resource — a connection pool, a Kafka consumer, a cache — will misbehave. Apply it to small stateless beans that read configuration, and restart the service for structural changes.

Related tutorials