Conclusion
Externalized configuration keeps every environment-specific value out of the deployable artifact, so the same build promotes unchanged from dev to prod and only its configuration differs — which means a value can change without a rebuild or (sometimes) even a redeploy. A config server turns configuration into a keyed lookup: the triple (application, profile, label) resolves to one merged property set, typically read from a versioned git repository, that clients fetch at startup and cache.
The one lookup to remember
The whole model is a keyed resolve-and-merge. A client boots and asks the config server for its properties by naming three things:
- application — which service (
orders). - profile — which environment/variant (
prod). Multiple profiles can be active (prod,eu). - label — which version of the config repo: a git branch, tag, or commit (
main). This is what makes rollback a git operation.
The server resolves the label, gathers every file that matches, and merges them most-specific-wins. Trace (orders, prod, main):
| # | Source (in precedence order) | Sets | Effective value after merge |
|---|---|---|---|
| 1 | application.yml (base, all services) | pool.size=10, log.level=INFO, timeout.ms=1000 | pool.size=10, log.level=INFO, timeout.ms=1000 |
| 2 | orders.yml (this app, all profiles) | pool.size=20, feature.x=off | pool.size=20, log.level=INFO, timeout.ms=1000, feature.x=off |
| 3 | orders-prod.yml (this app + profile) | log.level=WARN, feature.x=on | pool.size=20, log.level=WARN, timeout.ms=1000, feature.x=on |
| 4 | env var / CLI arg on the instance | timeout.ms=1500 | pool.size=20, log.level=WARN, timeout.ms=1500, feature.x=on |
Each later, more specific layer overrides keys from the earlier ones; keys it does not mention are inherited untouched. Point the same client at (orders, dev, main) and only the -dev layer swaps — the artifact is identical.
The four decisions that actually matter
1. Refresh: restart vs dynamic vs push
Fetching at startup is the simple, safe default: config is immutable for the life of the process, so every instance is self-consistent. To change a value you redeploy/restart. When you need change without a restart, clients can re-pull and rebind — in Spring Cloud that is @RefreshScope beans re-instantiated on an actuator /refresh call, often fanned out over a message bus. The cost is staleness and drift: a refresh is not atomic across a fleet, so for a window some instances run the new value and some the old. A dynamic system (Consul/etcd with watches) narrows that window toward sub-second but never fully closes it. Rule of thumb: make only safe-to-drift values dynamic (log levels, feature flags, timeouts); things that must change in lockstep (a schema-coupled setting) still want a controlled redeploy.
2. Precedence & override
Every externalized-config system defines a fixed layering (base < app < profile < instance-override). Know it cold: a "config not taking effect" bug is almost always a more-specific layer — an env var, a JVM arg, a profile file — silently winning. The override order is a feature (per-instance emergency tuning) and the top debugging trap.
3. Secrets
Plaintext secrets in git are the classic disaster: git is versioned and replicated, so a leaked DB password lives forever in history. Route secrets differently: store only encrypted values in the repo (server-side decryption) or, better, keep a reference in config and resolve the actual secret from a dedicated secrets manager (Vault, AWS Secrets Manager, sealed K8s secrets) with its own access control, rotation, and audit trail.
4. The config server is a startup dependency
If a service must reach the config server to boot, that server becomes a single point of failure exactly when it hurts most: during a mass restart or region recovery, every instance stampedes it at once. Two mitigations, used together: run it highly available (it is stateless over git, so replicate it freely), and have clients cache the last-known-good config locally so a server outage degrades startup rather than preventing it.
Pitfalls
- Secrets in plain config. A password committed to the config repo is now in git history on every clone and mirror. Encrypt or externalize to a vault — and rotate anything that was ever committed plain.
- Config server as a boot-time SPOF. "Fail fast if config unreachable" turns one server hiccup into a fleet-wide outage during a restart storm. Cache last-known-good and make the server HA.
- Silent dynamic-refresh drift. A
/refreshis not atomic across instances; for a window the fleet runs mixed values. If two settings must change together, a non-atomic refresh can leave an instance in an impossible combination. Redeploy such coupled changes. - Environment parity gaps. The point of one artifact is that only config differs — but if
devandprodconfig diverge in structure (keys present in one, absent in the other), you get bugs that only appear in prod. Keep the key set identical; vary only values. - No audit on who changed what. If config lives in a mutable KV store edited by hand, a bad value has no author and no diff. Git-backed config makes every change a reviewable, revertable commit — a real operational advantage over ad-hoc stores.
Choosing: what holds your config, and how it changes
Where the values live — four named options, cheapest first:
- Baked into the artifact (properties compiled/packaged in). Simplest, zero runtime dependency, but breaks the one-artifact rule — a value change means a rebuild, and the same jar can't run two environments. Use only for true constants that never vary by environment.
- Environment variables / Kubernetes ConfigMaps & Secrets. The 12-Factor default. Deploy-time config injected by the platform; no extra server to run, and it is the idiomatic choice when a config change already coincides with a redeploy (a ConfigMap change rolls the pods). Weak spots: no built-in history/diff, awkward for large or deeply structured config, and rebinding without a restart needs extra machinery.
- A config server (git-backed, pull + bus), e.g. Spring Cloud Config. Choose it when you want live change without redeploy plus audit and rollback for free (every change is a git commit). Costs: an extra HA service and a boot-time dependency to manage.
- A distributed KV store, e.g. Consul or etcd. Choose it when sub-second push matters — clients watch keys and react in near real time — and you already run it for service discovery. Costs: you operate a consensus cluster, and it is a mutable store, so you must add your own change-audit discipline.
Route secrets to a vault regardless of which of these you pick.
How values change is an orthogonal dial: static fetch at startup (immutable per process, self-consistent, redeploy to change) vs dynamic refresh (change without restart, at the price of cross-instance staleness). Pick static unless a concrete value genuinely needs to change under a running process.
If you never change a value between deploys, adopt none of this — a bundled properties file is correct, and a config server is complexity you will pay to operate for no benefit.
Takeaways
- Externalized config = one build artifact everywhere; a (application, profile, label) key resolves to a most-specific-wins merge of layered property sets, so environments differ only by the key.
- The permanent costs are structural: a boot-time network dependency (mitigate with HA + local last-known-good cache), cache staleness (a freshness-vs-atomicity dial), and a fleet-wide blast radius on a bad value (canary the rollout, revert the git commit).
- Static fetch is the safe default; make only drift-tolerant values dynamic, and always route secrets to a vault rather than plain git.
- Escalate deliberately: baked-in → env vars/ConfigMaps (deploy-time, restart fine) → git-backed config server (live change + audit/rollback) → Consul/etcd (sub-second push). If config never changes between deploys, use none of it.
Re-authored and deepened for this guide. Draws on the Twelve-Factor App (Factor III, "Config"), Chris Richardson, Microservices Patterns (Manning, 2018) and microservices.io (Externalized Configuration pattern), the Spring Cloud Config model of (application, profile, label) git-backed resolution with @RefreshScope / actuator refresh, and the HashiCorp Vault and Consul/etcd approaches to secrets and watch-based push. Replaces a content-free "Conclusion" summary with a decision-oriented synthesis.
🤖 Don't fully get this? Learn it with Claude
Stuck on Conclusion? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Conclusion** (System Design) and want to truly understand it. Explain Conclusion from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Conclusion** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Conclusion** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Conclusion** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.