Skip to content
ExponaBLOG
JAVA, SPRING FRAMEWORK & SPRING BOOT · DAY 073 / 100

Centralized config in Spring microservices: a deep dive

Config across many services without drift.

Expona Admin7 min read

Centralized config in Spring microservices: a deep dive

When you run five services instead of one, configuration becomes a coordination problem. Each service needs database credentials, API endpoints, feature flags, and timeout values, and keeping those synchronized without drift across environments is where teams start losing sleep at 2 AM. In this article, we'll walk through practical strategies for managing configuration across a Spring microservices architecture, from choosing your backing store to handling secrets safely and refreshing config without downtime.

Why centralization matters in a distributed world

In a monolith, you have one application.properties file. Environment-specific values live in separate files, and deployment pipelines substitute values at package time. That works until you're running twelve services across three environments, each with slightly different configurations. Without centralization, you end up with configuration scattered across property files, environment variables, and occasionally hardcoded in places nobody remembers. When you need to change a shared database connection string, you're updating multiple repositories and coordinating deployments.

Centralized configuration means one source of truth. Services pull their config from a single location at startup and (when it's safe) while running. This reduces human error, simplifies audits, and lets you adjust behavior across your entire platform without redeploying.

Spring Cloud Config: the built-in solution

Spring Cloud Config is the framework's native approach to centralized configuration. A dedicated config server reads from a Git repository, filesystem, or other backend and serves property values to client applications. Each microservice acts as a client, fetching its configuration on startup.

Here's a typical setup. First, you create a config server application:

java
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

In its application.yml, point it to your config source:

java
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/your-org/config-repo.git
          default-label: main
          search-paths: config

Your Git repository contains directories for each service and environment:

java
config/
  order-service-prod.yml
  order-service-dev.yml
  payment-service-prod.yml
  payment-service-dev.yml

Each client service declares dependency on spring-cloud-config-client and bootstrap.yml with its identity:

java
spring:
  application:
    name: order-service
  cloud:
    config:
      uri: http://config-server:8888
      fail-fast: true

The config server resolves {application-name}-{profile}.yml and serves it. During startup, order-service in the prod profile gets its values from order-service-prod.yml.

The advantage: change a timeout value, push to Git, and every client that fetches config sees the new value. No redeployment of the service itself. The trade-off is operational overhead. You're running another service, managing its availability, and ensuring Git access is reliable.

Platform-native alternatives: ConfigMaps and SecretsStore

If you're already in Kubernetes, ConfigMaps and Secrets are often a better fit. They integrate tightly with your platform and avoid adding another moving part to manage. A ConfigMap is a native Kubernetes object holding non-sensitive key-value data:

java
apiVersion: v1
kind: ConfigMap
metadata:
  name: order-service-config
data:
  DATABASE_URL: jdbc:postgresql://postgres:5432/orders
  CACHE_TTL_SECONDS: "3600"
  LOG_LEVEL: INFO

Your Spring application reads these through environment variables or mounts them as files. With the spring-cloud-kubernetes dependency, Spring automatically populates properties from ConfigMaps and Secrets matching your application name and profile.

java
spring:
  cloud:
    kubernetes:
      config:
        enabled: true
        sources:
          - name: order-service-config

This approach is lighter operationally. Kubernetes manages ConfigMaps natively, supports versioning and rollouts, and integrates with your existing infrastructure. The limitation: you're tightly coupled to Kubernetes, and this pattern doesn't help if you run services on VMs or multiple clouds.

For most teams running containerized microservices on a single platform, Kubernetes ConfigMaps are simpler and sufficient. If you're multi-cloud or have hybrid infrastructure, Spring Cloud Config or a dedicated tool like HashiCorp Consul gives you platform independence.

Secrets: never in source control

Configuration includes database passwords, API tokens, encryption keys, and signing secrets. These must never live in Git, even in a private repository. Every developer with repo access has them, and if the repo is ever compromised, your entire stack is exposed.

The rule is simple: use a secrets vault, not source control. HashiCorp Vault is industry-standard. Alternatively, cloud-native options like AWS Secrets Manager, Azure Key Vault, or Google Secret Manager integrate seamlessly with your infrastructure.

With Vault, your config server or platform config system retrieves secrets at runtime. In Kubernetes, Secrets objects hold sensitive data, mounted as files or environment variables:

java
apiVersion: v1
kind: Secret
metadata:
  name: order-service-secrets
type: Opaque
data:
  DATABASE_PASSWORD: cG9zdGdyZXNwYXNz  # base64-encoded
  STRIPE_API_KEY: c2tfdGVzdF8xMjM0NTY=

Your application accesses secrets through environment variables or mounted files. Spring's PropertySource mechanism pulls them in alongside regular config:

java
@Configuration
public class SecretConfig {
    
    @Value("${DATABASE_PASSWORD}")
    private String dbPassword;
    
    @Bean
    public DataSource dataSource() {
        // Use dbPassword to initialize connection pool
    }
}

One practice worth adopting: separate config (which changes occasionally) from secrets (which should rarely rotate). Store secrets in a vault. Store config in Git or ConfigMaps. This reduces friction around secret rotation without slowing down config updates.

Refresh without restart when safe

Some configuration changes are safe to apply while the application is running. Timeout values, log levels, feature flags, and retry policies can often be reloaded without side effects. Other changes—like database connection strings or encryption keys—require either a restart or careful coordination to avoid inconsistent state.

Spring Cloud Config Client supports the @RefreshScope annotation. Properties annotated with @RefreshScope are re-evaluated when the application receives a refresh request:

java
@Component
@RefreshScope
public class FeatureFlags {
    
    @Value("${feature.new-checkout:false}")
    private boolean newCheckout;
    
    @Value("${feature.recommended-products:false}")
    private boolean recommendedProducts;
    
    public boolean isNewCheckoutEnabled() {
        return newCheckout;
    }
    
    public boolean areRecommendedProductsEnabled() {
        return recommendedProducts;
    }
}

When config changes, post to the refresh endpoint:

java
curl -X POST http://order-service:8080/actuator/refresh

The application reloads scoped beans. Feature flags, timeouts, and rate limits reflect new values immediately. This is powerful for blue-green deployments, gradual rollouts, and quick incident response.

However, be cautious. If you refresh a cached database password, existing connections are stale. If you refresh a configuration that's read once at startup, the refresh has no effect. Document which properties are refresh-safe. In practice, restrict refresh to stateless configuration like features, timeouts, and log levels. Restart for changes that affect resource pools or critical initialization.

Keep defaults inside each service

Your centralized config system should hold environment-specific overrides, not every possible setting. Each service should have sensible defaults baked into its codebase. This achieves several things: services remain deployable and testable in isolation, you can reason about behavior without consulting the config server, and you reduce config bloat.

In application.yml inside the service JAR, define defaults for every configurable property:

java
app:
  database:
    connection-timeout: 10s
    idle-timeout: 30m
    max-pool-size: 20
  cache:
    ttl-seconds: 3600
    max-size: 10000
  security:
    jwt-expiry-minutes: 60
  logging:
    level: INFO

In your centralized config (Git or ConfigMaps), override only what differs per environment:

java
# order-service-prod.yml
app:
  database:
    max-pool-size: 100  # prod needs more connections
  cache:
    ttl-seconds: 7200   # longer TTL in prod
  logging:
    level: WARN         # reduce noise

When the service starts, Spring merges defaults with overrides. Prod gets the prod-specific values. Local development without a config server still has reasonable defaults. This pattern keeps services self-contained and config maintainable.

Common mistakes

Storing too much in config. Not every setting needs to be configurable. If a value changes once per quarter, hardcode it. Configuration is for values that change per environment or need runtime adjustment. Over-configuring makes your config files unmaintainable.

Putting secrets in Git. This happens often, usually unintentionally. A developer commits a database password, and weeks later it's discovered in the Git history. Use a secrets vault and treat Git as the home for code and non-sensitive config only. Scan your repository regularly with tools like git-secrets or TruffleHog.

Relying on config for dynamic behavior without testing. If you change a feature flag in production without testing the code path, you risk outages. Treat config changes like deployments—they should be intentional, auditable, and recoverable. Log config changes and alert when they occur.

Ignoring config server failure. If your Spring Cloud Config server is down, clients fail to start. Use fail-fast: false judiciously and ensure config servers are highly available. Alternatively, cache config locally so services degrade gracefully if the config server is unreachable.

Key takeaways

  • Centralized config in Spring microservices reduces drift and simplifies environment management. Choose between Spring Cloud Config (platform-agnostic) and platform-native solutions like Kubernetes ConfigMaps (simpler if containerized).
  • Secrets belong in a vault—HashiCorp Vault, AWS Secrets Manager, or Kubernetes Secrets. Never commit credentials to Git.
  • Use @RefreshScope for properties safe to reload at runtime (features, timeouts, log levels). Restart the service for changes affecting resource pools or critical initialization.
  • Defaults belong in application.yml inside each service. Centralized config holds environment-specific overrides. This keeps services self-contained and config DRY.
  • Monitor config servers for availability and log all configuration changes. Treat config updates with the same rigor as code deployments.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles