Two days ago OrderHub got a central Config Server. Yesterday the services learned to load-balance their own calls. But there was still a gap I hadn't closed: change a value in the config repo, and the running services have no idea. They bind their configuration once, at startup. To pick up a new value you'd redeploy every instance — an absurd price for flipping a feature flag or nudging a page size.
Day 23 fixes that, and then tackles the thing nobody should get wrong: keeping secrets out of the repo entirely.
Live refresh: two beans, one config change
The trick is Spring Cloud's refresh scope, riding on Actuator. First you expose the endpoint:
management:
endpoints:
web:
exposure:
include: health, circuitbreakers, refresh, env
A POST /actuator/refresh now tells the app to reload. Under the hood the ContextRefresher does two things: it rebuilds the Environment (re-fetching config from the server), then disposes every bean marked @RefreshScope. Those beans aren't eager singletons anymore — the real target is re-created lazily on the next access, reading the fresh Environment.
@Component
@RefreshScope
public class RefreshableOrderConfig {
public RefreshableOrderConfig(
@Value("${app.orders.greeting:OrderHub}") String greeting,
@Value("${app.orders.express-checkout-enabled:false}") boolean flag) {
// re-read on every refresh
}
}
To prove a refresh genuinely rebuilds the bean (rather than just re-reading a value that happened to change), I gave it a fingerprint — a static counter incremented in the constructor. A refresh re-creates the bean, so the count ticks up even when nothing changed. Next to it I put a StaticOrderConfig with the same @Value fields but no @RefreshScope: a plain singleton, frozen at startup values until the process restarts. A /api/config endpoint returns both side by side, so you can watch one update and the other stay stale.
That contrast is also what the test asserts, with no live server:
long before = refreshable.getInstanceSeq();
contextRefresher.refresh(); // == POST /actuator/refresh
assertThat(refreshable.getInstanceSeq()).isGreaterThan(before); // re-created
assertThat(staticConfig.getInstanceSeq()).isEqualTo(staticBefore); // frozen
@RefreshScope is opt-in on purpose: you annotate exactly the handful of beans that must flip live, and leave everything else safely frozen until a real deploy.
One instance at a time is fine for a demo. In production you run many, and hitting each by hand is fragile. That's what Spring Cloud Bus is for: link every instance over RabbitMQ or Kafka, and a single POST /actuator/busrefresh publishes a refresh event to the whole fleet. Wire a config-repo webhook to it and a git push becomes a cluster-wide refresh. I implemented single-service refresh here; the Bus is the drop-in scale-out.
Secrets: the repo is forever
Config lives in git, and git never forgets. A password or API key committed to the config repo — even in a private repo, even deleted in a later commit — is leaked. It's in the history, it's scraped by bots the instant it hits a remote, and "rotating" it means a commit. The rule is absolute: configuration goes in the repo, secrets do not.
So I pulled the shipping API key out and left only a reference behind:
app:
orders:
integrations:
shipping-api-key: ${SHIPPING_API_KEY:local-dev-placeholder-not-a-real-secret}
The committed value is a placeholder. At runtime Spring resolves it from the SHIPPING_API_KEY environment variable, injected by the platform and never committed. The safe default keeps local dev and tests booting without the real key; in production you drop it so a missing var fails loudly instead of silently running with a fake. The application code just reads a property — it has no idea the value came from the environment. And it never logs or returns the raw value; a SecretMasker turns it into a fingerprint (sk_l…N1) for diagnostics.
Two heavier options are worth knowing. Config Server encryption lets you commit {cipher}AQB... values that the server decrypts with an encrypt.key held outside the repo — only ciphertext is committed. And Vault / AWS Secrets Manager is the real production answer: access policies, an audit trail, automatic rotation, even short-lived dynamic credentials. Spring Cloud Vault plugs in as another PropertySource, so ${...} resolution in your code is unchanged. That indirection is the whole point — you can swap the backend from env var to {cipher} to Vault without touching a line of application code.
The frontend half
On the frontend, "environment variable" means something completely different. A Vite VITE_* var is inlined into the bundle at build time — it ships to every browser and anyone can read it. So the frontend never holds a secret: privileged calls are proxied through the backend that holds the real credential, and runtime feature flags come from a small backend endpoint (backed by that same refreshable config), so product can flip a feature live with no frontend rebuild.
Config you can change freely, secrets you cannot leak, and a client that stays dumb about both.
Live demo: https://dev48v.infy.uk/orderhub.php
Code: https://github.com/dev48v/order-hub-from-zero
Next up, Day 24: inter-service authentication with service tokens.
Top comments (0)