skip to content

Explain how a config change ends up rebinding a @RefreshScope / @ConfigurationProperties bean, without a Config Server.

level: seniorimportance: should knowfreq 40%

answer

  1. Kubernetes IS the backend — no Config Server
  2. RefreshEvent -> ContextRefresher.refresh()
  3. @ConfigurationProperties rebound in place
  4. @RefreshScope target disposed -> lazily recreated
  5. same machinery as POST /actuator/refresh

basics

~20 s

The reload watcher detects the ConfigMap change and publishes a RefreshEvent. Spring Cloud's ContextRefresher rebuilds the Environment, re-binds @ConfigurationProperties beans, and destroys @RefreshScope beans so they are recreated with the new values on next use.

solid answer

~40 s

There's no Config Server in the loop — Kubernetes IS the config backend. When the reload watcher (event or polling) sees a changed ConfigMap/Secret, it fires a Spring `RefreshEvent`. The `RefreshEventListener` calls `ContextRefresher.refresh()`, which re-creates the property sources (re-reading the ConfigMap into a fresh `ConfigMapPropertySource`), then does two things: it rebinds every `@ConfigurationProperties` bean in place via the binder, and it clears the `refresh` bean scope, disposing each `@RefreshScope` proxy target so the next method call lazily rebuilds the target with current properties. Requests already inside a refresh-scoped bean finish on the old instance; new calls get the new one. This is identical to the machinery behind `POST /actuator/refresh` — Spring Cloud Kubernetes just supplies the trigger instead of a Config Server bus event.

code

java · 28 lines
java
// A refresh-aware bean: values update on reload without a restart.
@ConfigurationProperties(prefix = "orders")
public class OrderProps {
    private int maxItems;        // rebound in place on refresh
    public int getMaxItems() { return maxItems; }
    public void setMaxItems(int v) { this.maxItems = v; }
}

@Service
@RefreshScope                     // proxy target recreated after a RefreshEvent
public class OrderPolicy {
    private final int maxItems;
    // captured at (re)construction; a NEW target is built after refresh
    public OrderPolicy(OrderProps props) {
        this.maxItems = props.getMaxItems();
    }
    public boolean allows(int count) { return count <= maxItems; }
}

// Contrast: this bean would NOT update on refresh —
// it captured @Value once and is a plain singleton.
@Service
class StalePolicy {
    private final int maxItems;
    StalePolicy(@Value("${orders.max-items}") int maxItems) {
        this.maxItems = maxItems; // frozen at first construction
    }
}

go deeper

for a junior

Know that a change fires a RefreshEvent and refresh-scoped beans get rebuilt.

for a middle

Explain @ConfigurationProperties rebind vs @RefreshScope recreation and that it's the /actuator/refresh machinery.

for a senior

Walk ContextRefresher steps, lazy target swap semantics, and which beans are not rebound.

for a principal

Discuss atomicity/ordering, proxy caveats, idempotent re-init, and when to prefer restart_context for non-refreshable infra beans.

**The big idea.** With a git-backed Config Server you'd watch the server and `/refresh`. Here Kubernetes ConfigMaps/Secrets ARE the config store, so the trigger comes from watching cluster objects — but the *rebind* mechanism downstream is the exact same Spring Cloud refresh machinery. **Step by step:** 1. **Detection.** The reload component (`event` watch/informer or `polling` scheduler) notices the ConfigMap/Secret changed and that the new content differs from what's loaded. 2. **Event.** It publishes an application `RefreshEvent`. Spring Cloud's `RefreshEventListener` receives it. 3. **ContextRefresher.refresh().** This: a. Builds a *new* `Environment` in a throwaway context, capturing the set of changed keys. b. Replaces the reloadable property sources in the live `Environment` — the `ConfigMapPropertySource`/`SecretsPropertySource` are re-read with fresh cluster data. c. **Rebinds `@ConfigurationProperties` beans.** The `ConfigurationPropertiesRebinder` re-runs binding on each such bean, so its fields reflect new values *in place* (same bean instance). d. **Clears the refresh scope.** `RefreshScope` is a custom Spring bean scope. Each `@RefreshScope` bean is a proxy backed by a cached target. `refresh()` disposes those cached targets (calling destruction callbacks). The proxy stays; the next method invocation triggers lazy re-creation of the target, wired with the now-current properties. 4. **Change events.** An `EnvironmentChangeEvent` carries the set of changed keys for anything that wants to react programmatically. **Concurrency semantics.** Because `@RefreshScope` swaps the target lazily, a thread mid-call on the old target completes against old config; subsequent calls resolve the new target. This avoids tearing but means eventual, not atomic, cutover. **What is NOT rebound.** Plain singletons that read `@Value` once at construction; beans created by `@Bean` factory methods that captured config at build time; connection pools/clients built in a non-refresh singleton. To make those responsive, annotate the holder `@RefreshScope` or route config through a `@ConfigurationProperties` bean the pool reads on each use. **Actuator parity.** `POST /actuator/refresh` invokes the same `ContextRefresher`. So you can reason about Kubernetes reload as "an automatic caller of `/refresh`." Testing tip: hit `/actuator/refresh` manually to validate your beans are refresh-aware before relying on cluster watches. **Gotchas.** - `@RefreshScope` beans are proxied — beware `final` classes/methods and self-invocation, same caveats as any Spring proxy. - Rebinding runs application `@PostConstruct`/`@RefreshScope` re-init logic; make it idempotent and cheap. - Datasource/URL changes often need `restart_context` because the pool isn't easily refresh-scoped. - Ordering matters: if the same key is in a Secret and ConfigMap, the winner after refresh depends on property-source order. **When to lean on this.** Feature flags, log levels, timeouts, thresholds — values safe to swap live. For structural changes (datasource, listeners) prefer `restart_context` or a pod restart.

  • How is this different from doing the same thing with a git-backed Config Server?
    The downstream rebind (`ContextRefresher`, `@RefreshScope`, `@ConfigurationProperties`) is identical. The difference is the source and trigger: Config Server serves config from git and you refresh via `/actuator/refresh` or Spring Cloud Bus; Spring Cloud Kubernetes reads ConfigMaps/Secrets directly and triggers refresh from a Kubernetes watch/poll — no separate server.
  • Why does a thread already executing inside a @RefreshScope bean not see the new config immediately?
    Refresh disposes the cached target but the currently-executing target instance keeps running; only the next proxy invocation resolves and builds the new target. Cutover is lazy and per-call, not atomic across in-flight work.

saying these in an interview costs you the question

  • Thinking a Config Server is required for refresh to work in Kubernetes
  • Assuming all beans update automatically
  • Believing refresh is atomic across in-flight requests

context