On /actuator/refresh, which beans pick up new values — @Value beans vs @ConfigurationProperties beans — and why?
answer
- @ConfigurationProperties = auto-rebind, no @RefreshScope
- @Value = needs @RefreshScope
- rebinder listens to EnvironmentChangeEvent
- rebind = same instance; refreshScope = new instance
- @Validated re-runs on rebind
basics
~10 s@ConfigurationProperties beans are automatically rebound with new values on refresh, no annotation needed. A bean using @Value only picks up new values if you also put @RefreshScope on it.
solid answer
~40 sThere are two distinct mechanisms. @ConfigurationProperties beans get their fields re-bound automatically: a ConfigurationPropertiesRebinder listens for EnvironmentChangeEvent (fired during refresh) and re-runs binding on each @ConfigurationProperties bean in place — so you do NOT need @RefreshScope for them. Plain @Value fields are resolved once at construction and are not rebound by that mechanism; to update them you must annotate the bean with @RefreshScope, so the whole bean is evicted and recreated (with fresh @Value resolution) on the next call. Practical guidance: prefer @ConfigurationProperties for tunables you want refreshable — it's cleaner, type-safe, and refreshes in place without proxy re-creation. Reserve @RefreshScope for beans built from @Value or that must be fully rebuilt (e.g., to re-open a resource) when config changes.
code
java · 23 lines// (A) Auto-refreshed WITHOUT @RefreshScope — rebound on EnvironmentChangeEvent
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "app.rate")
public class RateProps {
private int limit; // gets new value after /actuator/refresh
public int getLimit() { return limit; }
public void setLimit(int l) { this.limit = l; }
}
// (B) @Value bean — stale after refresh UNLESS annotated @RefreshScope
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
@Component
@RefreshScope // remove this and 'limit' stays at its startup value
public class RateGuard {
@Value("${app.rate.limit:100}")
private int limit;
public int current() { return limit; }
}go deeper
Remember the rule of thumb: @ConfigurationProperties refreshes by itself; @Value needs @RefreshScope.
Explain the two mechanisms — in-place rebind via EnvironmentChangeEvent vs eviction+recreate via refresh scope.
Name ConfigurationPropertiesRebinder and discuss validation/immutable-binding edge cases.
Set a team convention (prefer @ConfigurationProperties) and reason about failure modes when rebind validation throws.
This is one of the most commonly misunderstood parts of Spring Cloud refresh. **Two independent update paths.** 1. **@ConfigurationProperties — automatic rebind.** When you have a `@ConfigurationProperties("app")` bean, Spring Cloud registers a `ConfigurationPropertiesRebinder` (an `ApplicationListener<EnvironmentChangeEvent>`). During `/actuator/refresh`, `ContextRefresher` publishes an `EnvironmentChangeEvent`. The rebinder catches it and, for each `@ConfigurationProperties` bean, **destroys and re-initializes the binding on the same instance** — effectively calling the binder again so fields get the new values. No `@RefreshScope` and no proxy are involved; the object identity stays the same, only its bound fields change. This is why `@ConfigurationProperties` beans are 'refreshable out of the box'. 2. **@Value — needs @RefreshScope.** A `@Value("${...}")` placeholder is resolved by the container when the bean is created and written into the field. Nothing re-runs that resolution on `EnvironmentChangeEvent`. So a plain `@Value` bean keeps its old value after refresh. Adding `@RefreshScope` fixes it a different way: the bean is a refresh-scoped proxy, `refreshScope.refreshAll()` evicts it, and the next call constructs a brand-new instance — construction resolves `@Value` against the now-updated `Environment`. **Key contrast.** - `@ConfigurationProperties`: *same instance, re-bound in place*, driven by `EnvironmentChangeEvent`. - `@RefreshScope` + `@Value`: *new instance*, driven by scope eviction (`RefreshScopeRefreshedEvent`). **Gotchas.** - You *can* combine `@RefreshScope` with `@ConfigurationProperties`, but it's usually redundant — the rebinder already handles it. Use it only if you specifically need a full re-creation (e.g., re-run `@PostConstruct` / re-open a connection) on refresh. - Validation: `@Validated` on `@ConfigurationProperties` runs again on rebind; if the new values fail validation the rebind throws and the bean can be left with old (or partially bound) state — handle failures. - Relaxed/immutable binding: constructor-bound (`@ConstructorBinding`, immutable) `@ConfigurationProperties` still rebind because the rebinder recreates the binding target. - Beans that copy a `@Value` into another field, cache it, or pass it to a resource at startup won't reflect changes even under `@RefreshScope` unless the copy happens in the constructor of the recreated bean. **When to use which.** Default to `@ConfigurationProperties` for refreshable config: type safety, no proxy, in-place update. Use `@RefreshScope` when the bean is `@Value`-driven or must be fully torn down and rebuilt when configuration changes.
- Which component actually rebinds @ConfigurationProperties beans, and on which event?ConfigurationPropertiesRebinder, an ApplicationListener<EnvironmentChangeEvent>. When refresh publishes EnvironmentChangeEvent it re-runs binding on each @ConfigurationProperties bean in place.
- Is it ever useful to put @RefreshScope on a @ConfigurationProperties bean?Rarely. The rebinder already updates the fields. Add @RefreshScope only if you need a full bean re-creation on refresh — e.g., to re-run @PostConstruct or re-open a resource derived from the properties.
saying these in an interview costs you the question
- Claiming @ConfigurationProperties needs @RefreshScope to refresh
- Claiming @Value fields auto-refresh without @RefreshScope
- Saying @ConfigurationProperties beans are recreated as new instances (they're rebound in place)