Delving into Code An Example
Spring Cloud Config works because a lightweight config server owns a git repository of property files, and every microservice, at startup, calls that server over HTTP with its own identity ({application}/{profile}/{label}) to pull the matching file into its Spring Environment before any bean is wired — so configuration lives outside the deployable artifact and can be versioned, audited, and changed without rebuilding the service.
The config server
Two things make an app a config server: the spring-cloud-config-server dependency and the @EnableConfigServer annotation. This annotation is real and it does exactly what it says — it registers the controllers that expose the resolution endpoints.
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}Point it at a git repo and pick a port in application.yml:
server:
port: 8888
spring:
cloud:
config:
server:
git:
uri: https://github.com/acme/config-repo
default-label: mainThe repo holds one file per service-and-environment, named by convention {application}-{profile}.yml. For an order service in production, order-service-prod.yml might contain:
# order-service-prod.yml (in the git repo)
payment:
timeout-ms: 3000
gateway-url: https://pay.acme.comThe client — no @EnableConfigClient exists
Here is the correction to the widely-copied mistake: there is no @EnableConfigClient annotation in Spring Cloud Config. Adding it will not compile. A client needs only the starter dependency plus a small amount of configuration that tells it (a) who it is and (b) where the server lives.
// build.gradle
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.boot:spring-boot-starter-actuator'In Spring Boot 2.4+ the server is reached via spring.config.import (the old bootstrap.yml approach needs the extra spring-cloud-starter-bootstrap dependency):
# application.yml of the CLIENT
spring:
application:
name: order-service # → the {application} slug
profiles:
active: prod # → the {profile} slug
config:
import: "configserver:http://localhost:8888"
management:
endpoints:
web:
exposure:
include: refresh # so /actuator/refresh is reachableThe main class stays a plain @SpringBootApplication — no config-client annotation at all.
Reading a value — and why @Value alone won't refresh
@Value injects a resolved property into a field once, at bean construction:
@RefreshScope // REQUIRED for /actuator/refresh to re-bind this bean
@Component
public class PaymentClient {
@Value("${payment.timeout-ms}")
private int timeoutMs; // 3000 at startup, from order-service-prod.yml
public int timeoutMs() { return timeoutMs; }
}Why the naive version is wrong: a common claim is that POST /actuator/refresh gives you "config changes without a restart" for every bean. It does not. Refresh fires an EnvironmentChangeEvent and only re-instantiates beans annotated @RefreshScope (plus it re-binds @ConfigurationProperties beans, which get refresh behaviour for free). A plain singleton with a bare @Value field keeps its old value forever until the process restarts, because that field was set once during construction and nothing tells Spring to rebuild the bean. Drop the @RefreshScope above and the timeout stays 3000 no matter how many times you hit refresh.
A concrete trace: change a timeout live
| # | Action / call | What happens | timeoutMs |
|---|---|---|---|
| 1 | order-service boots | Client reads spring.config.import, calls GET http://localhost:8888/order-service/prod | — |
| 2 | Server resolves | Clones/pulls the git repo, reads order-service-prod.yml, returns JSON property sources | — |
| 3 | Client merges | Values land in the Environment; PaymentClient is constructed | 3000 |
| 4 | Ops edits git | payment.timeout-ms: 5000, commit + push to main | 3000 (stale) |
| 5 | POST /actuator/refresh | Client re-fetches from server, fires EnvironmentChangeEvent; @RefreshScope bean is discarded | 3000 → pending |
| 6 | Next call to timeoutMs() | PaymentClient is lazily re-created with the new value | 5000 |
Note step 6: @RefreshScope beans are rebuilt lazily on next access, not eagerly at refresh time — the in-flight request that triggered refresh may still see the old bean.
Encrypting secrets
The encrypt.key is a symmetric key set on the server — and you should supply it as an environment variable (ENCRYPT_KEY=...), never commit it to the config repo, since it decrypts everything. With it set, the server exposes /encrypt and /decrypt:
$ curl -s http://localhost:8888/encrypt -d 'db-password-123'
AQBc9f2e7a... # opaque cipher textStore the cipher in git with a {cipher} prefix; the server decrypts it before handing the value to any client, so the client sees plaintext and code is unchanged:
# order-service-prod.yml
spring:
datasource:
password: '{cipher}AQBc9f2e7a...'For real deployments, prefer an asymmetric RSA keystore (encrypt.key-store.*) so servers can serve decrypted values without any node holding the symmetric secret.
Pitfalls
- Reaching for
@EnableConfigClient. It does not exist; the client is activated purely by the starter dependency plusspring.config.import(or a bootstrap file). Copying it from a blog post yields a compile error. - Expecting bare
@Valueto update on refresh. Without@RefreshScope(or moving the properties into a@ConfigurationPropertiesbean),/actuator/refreshre-fetches config but your field never changes. - Forgetting to expose the endpoint.
/actuator/refreshreturns 404 unlessmanagement.endpoints.web.exposure.includelistsrefresh— and it must be POST, not GET. - The server as a boot-time SPOF. If the config server is down when a client starts, the client fails to boot unless you enable fail-fast + retry or allow it to run on cached/default values. This turns config into an availability dependency.
- Wrong
spring.application.name. The name is the{application}slug used to pick the file. A typo silently serves onlyapplication.yml(the global default) and your service-specific values vanish with no error. - Refresh is per-instance. One POST refreshes one process. With 20 replicas behind a load balancer you must fan the refresh out to all of them (that is what Spring Cloud Bus + a message broker automates via
/actuator/busrefresh).
When to use it — and when not to
Signals that point to a config server: you are already in the Spring ecosystem; you want config versioned in git with commit history and rollback; you have many services sharing common values; and you want to flip a value (a feature flag, a timeout) without a redeploy pipeline.
Trade-offs vs. Kubernetes ConfigMaps/Secrets. ConfigMaps mount config as env vars or files with zero extra infrastructure and are the natural fit if you are already on k8s. What you gain with them: simplicity and no runtime dependency. What you lose: git-native history is not built in, and updating a ConfigMap typically means rolling the pods (a restart) — you trade dynamic refresh for immutability. Choose Spring Cloud Config when you want git-backed audit trails and live refresh across a Spring fleet; prefer ConfigMaps when you are k8s-native and a redeploy-per-change is acceptable.
Trade-offs vs. HashiCorp Vault / AWS Parameter Store. Config Server's encryption is bolted on: a shared symmetric key and manual /encrypt round-trips, with git as the store (no fine-grained per-secret ACLs, no leasing, no rotation). Vault gives dynamic short-lived secrets, strong access policies, and audit logging — at the cost of running and operating Vault. Choose Vault when secrets are the primary concern; keep Config Server for non-secret application config (and even then, Spring integrates with Vault as a backend, so the two compose).
Takeaways
- A client becomes a config client through the
spring-cloud-starter-configdependency andspring.config.import— never a@EnableConfigClientannotation, which does not exist. - The server resolves files by
{application}-{profile}, driven by the client'sspring.application.nameand active profile. /actuator/refreshre-fetches config but only rebuilds@RefreshScope(and@ConfigurationProperties) beans; plain@Valuesingletons stay stale until restart, and refresh is one-instance-at-a-time.- Encryption uses a server-side key (kept out of git) plus
/encrypt+{cipher}markers; for serious secret management, reach for Vault instead.
Re-authored and deepened for this guide. Sources: Spring Cloud Config reference documentation (docs.spring.io, Config Server & Client, Encryption/Decryption, and Refresh Scope sections); Spring Boot Actuator reference (the refresh endpoint and endpoint exposure); Spring Cloud Bus documentation (busrefresh fan-out); and Chris Richardson, Microservices Patterns (Manning), Externalized Configuration pattern. Corrects the non-existent @EnableConfigClient annotation and the incomplete "refresh without restart" claim.
🤖 Don't fully get this? Learn it with Claude
Stuck on Delving into Code An Example? 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 **Delving into Code An Example** (System Design) and want to truly understand it. Explain Delving into Code An Example 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 **Delving into Code An Example** 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 **Delving into Code An Example** 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 **Delving into Code An Example** 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.