Distributed Configuration
In a monolith, configuration lives in one place and restarts propagate it instantly. In a distributed system, dozens of services each carry their own copy — and keeping them consistent, up-to-date, and environment-aware without a restart is a hard problem. Distributed configuration management solves it by externalising config into a shared source of truth that services read from (pull) or are notified by (push).
The Problem
As a system grows, configuration scattered across services creates several failure modes:
- Configuration drift — services deployed at different times accumulate different config values. Environment variables set manually on one host silently diverge from others. What works in staging breaks in production.
- Restart-coupled changes — baking config into the image or environment variables means changing a timeout or a feature flag requires a full redeploy, even when the change is trivial.
- Secret sprawl — credentials duplicated across services and environments are hard to rotate and easy to leak.
- No audit trail — ad-hoc config changes have no history, no rollback, and no accountability.
Pull vs Push
There are two fundamental models for how services receive updated configuration:
| Pull | Push | |
|---|---|---|
| How it works | Service polls the config source on startup (and optionally on an interval) | Config source notifies services when a value changes |
| Latency | Delayed by poll interval — stale until next check | Near real-time — services receive the change immediately |
| Complexity | Simple — service just reads a URL on boot | Requires a pub/sub channel or long-poll connection per service instance |
| Restart required? | Yes — unless the service supports live reload | No — service reacts to the push event at runtime |
| Examples | Spring Cloud Config (default), AWS SSM Parameter Store | Spring Cloud Bus + Config, etcd watches, AWS AppConfig, LaunchDarkly |
In practice most systems use a hybrid: pull on startup for baseline config, push for runtime changes that must propagate without a restart (feature flags, rate limits, kill switches).
Config Server (Spring Cloud)
Spring Cloud Config provides a Git-backed config server and a client library that makes externalised configuration the default in a Spring Boot microservices stack.
Server setup
// build.gradle
implementation 'org.springframework.cloud:spring-cloud-config-server'
// ConfigServerApplication.java
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}# application.yml (config server)
server:
port: 8888
spring:
cloud:
config:
server:
git:
uri: https://github.com/your-org/config-repo
clone-on-start: true
default-label: mainThe server exposes config over HTTP at {service-name}/{profile}. Clients fetch their config on startup: GET /order-service/production.
Client setup (pull)
// build.gradle
implementation 'org.springframework.cloud:spring-cloud-starter-config'
# bootstrap.yml (client — loaded before application.yml)
spring:
application:
name: order-service
config:
import: optional:configserver:http://config-server:8888Push with Spring Cloud Bus
To push changes at runtime without restarting services, connect them via a message broker (RabbitMQ or Kafka). A POST /actuator/busrefresh on any service broadcasts a refresh event to all subscribers.
# add to every service
implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp'
# application.yml (each service)
spring:
rabbitmq:
host: rabbitmq
port: 5672
management:
endpoints:
web:
exposure:
include: busrefresh# trigger a push refresh across all services
POST http://any-service:PORT/actuator/busrefreshAWS AppConfig (managed push)
For AWS environments, AppConfig manages config as versioned deployments with built-in rollout strategies (linear, exponential, all-at-once) and automatic rollback on CloudWatch alarms. Clients poll a local agent sidecar which handles the push lifecycle internally.
// Java SDK — poll the local AppConfig agent (runs as a sidecar)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(
"http://localhost:2772/applications/MyApp" +
"/environments/Production/configurations/MyConfig"))
.build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
String configJson = response.body();Feature Flags
Feature flags are push-based configuration taken to its logical conclusion: any boolean or multi-variant value that can be changed in production without a deploy, per user, per cohort, or per environment.
- Release flags — hide an incomplete feature behind a flag so it ships to production dark and is enabled when ready, decoupling deploy from release.
- Kill switches — disable a non-critical feature instantly if it causes errors, without a rollback deploy.
- Canary / percentage rollouts — enable a feature for 5% of users, monitor metrics, and expand incrementally.
- A/B testing — serve different variants to different user segments and measure which performs better.
LaunchDarkly (Java SDK)
LDConfig config = new LDConfig.Builder().build();
LDClient client = new LDClient("sdk-key", config);
LDContext context = LDContext.builder("user-123")
.set("plan", "premium")
.build();
boolean showNewCheckout = client.boolVariation("new-checkout-flow", context, false);
if (showNewCheckout) {
return newCheckoutService.render(cart);
} else {
return legacyCheckoutService.render(cart);
}LaunchDarkly streams flag changes to connected SDK instances — the flag value returned by boolVariation reflects the live state, no restart or poll needed.
Best Practices
- Never bake secrets into config files. Use a dedicated secrets manager (AWS Secrets Manager, HashiCorp Vault) for credentials and inject them at runtime. Config files in Git should hold non-sensitive settings only.
- Version and audit every change. A Git-backed config server gives you a full history, diff, and rollback for free. For managed services, ensure CloudTrail or equivalent is on.
- Validate config before it ships. A bad config value should fail loudly on the server, not silently corrupt live services. Schema validation and staged rollouts (AppConfig deployments, flag percentage rollouts) reduce blast radius.
- Design services to tolerate stale config. If the config server is unavailable, services should start from a cached local copy rather than refusing to boot. Spring Cloud Config supports this with
spring.cloud.config.fail-fast=falseand a local fallback. - Clean up flags. Feature flags are technical debt. Every live flag is a branch in the code. Permanent flags should become proper config; dead flags should be deleted.