What are the concurrency and lifecycle gotchas of @RefreshScope beans — in-flight requests, @PostConstruct, and beans holding expensive resources?
answer
- lazy re-create -> not atomic, two instances briefly
- @PreDestroy old + @PostConstruct new re-run each refresh
- first caller pays construction latency
- resources/pools rebuilt, in-memory state lost
- read value once for per-request consistency
basics
~20 sA refresh evicts the bean but recreation is lazy, so a request already running keeps the old instance. On recreation, the old bean's @PreDestroy runs and the new bean's @PostConstruct runs again. Beans that open connections or pools get torn down and rebuilt, which can be costly.
solid answer
~40 sBecause refresh-scoped beans are proxies with lazy re-creation, refresh is not atomic across a workflow. A request that already resolved the target keeps executing on the old instance; a concurrent request arriving after eviction triggers construction of a new instance — so two versions can briefly coexist. On eviction the old instance's DisposableBean/@PreDestroy runs; the new instance's constructor and @PostConstruct run on first access. That makes @RefreshScope powerful for beans that must fully re-initialize (re-open a datasource, rebuild a client) but risky for beans holding expensive or stateful resources: every refresh discards in-memory state and pays the rebuild cost, and if @PostConstruct is slow the first post-refresh caller eats the latency. Guard shared mutable state, keep construction cheap, and don't put @RefreshScope on beans whose identity other components capture outside the proxy.
code
java · 28 linesimport jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@Component
@RefreshScope
public class HttpClientHolder {
@Value("${downstream.base-url}")
private String baseUrl;
private ExpensiveClient client;
@PostConstruct // re-runs on EVERY refresh (lazily, on first use)
void init() {
this.client = ExpensiveClient.connect(baseUrl); // costly rebuild each refresh
}
@PreDestroy // runs when the old instance is evicted on refresh
void close() {
if (client != null) client.close();
}
public Response call() { return client.get(); }
}
// Gotcha: a request already holding the old instance keeps using the old baseUrl
// until it returns; the next call builds a fresh client against the new URL.go deeper
Know that recreation is lazy and lifecycle callbacks re-run on refresh.
Explain in-flight requests keep the old instance and resources get rebuilt.
Discuss two-instances-coexisting, @PostConstruct re-running, and per-request consistency mitigation.
Set policy on what may be refresh-scoped, budget refresh latency, and guard shared mutable state across refreshes.
**Why these gotchas exist.** `@RefreshScope` gives you a proxy over a lazily-created target held in the `refresh` scope cache. `refreshScope.refreshAll()` (on `/actuator/refresh`) evicts targets; re-creation happens on the *next* method call through the proxy. **1. In-flight requests / non-atomic refresh.** The proxy resolves the target at the moment a method is invoked. If a thread already grabbed the old instance and is mid-execution, it finishes on that old instance even though a refresh happened. A different thread calling after eviction builds and uses a *new* instance. So during/after a refresh, **two instances of the same bean can be live simultaneously**. If the bean has shared mutable state or coordinates external ordering, this can cause subtle bugs. For multi-step business logic reading a refresh-scoped value several times within one request, the value could differ across steps if a refresh lands mid-request — capture it once locally if you need consistency. **2. Lifecycle callbacks re-run.** - Eviction destroys the old target: `DisposableBean.destroy()` / `@PreDestroy` run for it. - First access after eviction constructs a new target: constructor injection, `@Value` resolution, `InitializingBean.afterPropertiesSet()` / `@PostConstruct` all run **again**. So any one-time-at-startup assumption in `@PostConstruct` is violated — it will fire on every refresh cycle (for beans that get re-accessed). Make init idempotent and cheap. **3. Expensive / stateful resources.** Spring Cloud actually leverages this — e.g., a `DataSource` can be refresh-scoped so a changed URL/credentials rebuild the pool. But it cuts both ways: refreshing discards connection pools, caches, counters, in-memory buffers, and rebuilds them. Costs: connection re-establishment, cache cold-start, lost in-memory state. The **first caller** after refresh pays the construction + `@PostConstruct` latency (lazy). Never assume refresh is free. **4. Proxy escape / captured identity.** If some component captures the *real* target (e.g., via `AopProxyUtils.getSingletonTarget`, or a callback registered with `this` from inside the bean) rather than going through the proxy, it will keep the stale instance after refresh. Always interact through the injected proxy. **5. Thread-safety of construction.** Concurrent first-access after eviction is guarded so a single instance is built, but your constructor/`@PostConstruct` should not have side effects that break if invoked repeatedly across refreshes. **6. Startup ordering with @PostConstruct on the proxy.** A refresh-scoped bean is created lazily, so its `@PostConstruct` does **not** run at application startup — it runs on first use. If you rely on eager init (e.g., registering something at boot), `@RefreshScope` defers that until first access, which can surprise you. **Design guidance.** Use `@RefreshScope` when full re-initialization on config change is the goal (re-open resource, rebuild client). Keep the constructor/`@PostConstruct` cheap and idempotent. Avoid it for hot-path beans with heavy init or important in-memory state; prefer `@ConfigurationProperties` (in-place rebind, no re-creation) for pure value tunables. For values needing per-request consistency, read once into a local variable.
- Does a refresh-scoped bean's @PostConstruct run at application startup?No. Refresh-scoped beans are created lazily, so @PostConstruct runs on first access, not at boot — and again after each refresh when the bean is next used.
- How do you keep a config value consistent across a multi-step request that might overlap a refresh?Read it once into a local variable at the start of the operation instead of re-reading the refresh-scoped bean at each step, so a mid-request refresh can't change it under you.
saying these in an interview costs you the question
- Assuming refresh atomically swaps the bean for all in-flight threads at once
- Thinking @PostConstruct only runs once and won't re-run on refresh
- Believing a refresh is cheap even when the bean opens pools/connections