Why do constructor-injection circular dependencies fail while field/setter cycles can be resolved? Explain in terms of Spring's bean creation phases.
answer
- instantiate → addSingletonFactory → populate → initialize
- early ref needs a constructed object first
- constructor arg needed at step 1, too early
- singletonsCurrentlyInCreation guard
- BeanCurrentlyInCreationException
basics
~20 sSpring builds a bean in two steps: construct it, then inject its dependencies. Constructor injection needs the dependency at step one, before any object exists to expose, so the cycle can't be broken. Field/setter injection happens at step two, after a half-built object exists to hand out.
solid answer
~40 sIn AbstractAutowireCapableBeanFactory, creating a singleton is: instantiate (call constructor) → addSingletonFactory (register an early-reference ObjectFactory) → populateBean (inject fields/setters) → initializeBean. The early reference can only be registered after the constructor returns, because you need an actual object to expose. For field/setter cycles, A is constructed, its early reference is cached, then during A's population B is created and receives that early A — the loop closes. For constructor cycles, A's constructor argument is B and B's constructor argument is A; Spring must resolve B fully before A's constructor can even run, and vice versa. No object ever reaches the addSingletonFactory step, so there's nothing to expose. Spring detects the in-progress creation via the singletonsCurrentlyInCreation set and throws BeanCurrentlyInCreationException.
code
java · 15 lines// Fails at startup: neither constructor can run first
@Component
class OrderService {
OrderService(PaymentService p) {}
}
@Component
class PaymentService {
PaymentService(OrderService o) {}
}
// Fix without redesign: @Lazy proxy on one constructor param
@Component
class OrderServiceFixed {
OrderServiceFixed(@Lazy PaymentService p) {} // p is a proxy; real bean resolved on first call
}go deeper
Know that constructor cycles fail and setter/field cycles can resolve.
Explain the instantiate-then-populate ordering and why the early reference only exists after construction.
Name doCreateBean's steps and the singletonsCurrentlyInCreation guard precisely.
Discuss why failing fast on constructor cycles is a feature, and trade-offs of @Lazy vs redesign.
## The two-phase creation model Spring's `AbstractAutowireCapableBeanFactory.doCreateBean` runs roughly: ``` 1. instanceWrapper = createBeanInstance(...) // CONSTRUCTOR runs here 2. addSingletonFactory(beanName, () -> getEarlyBeanReference(...)) // expose early ref 3. populateBean(...) // field & setter injection 4. exposedObject = initializeBean(...) // BeanPostProcessors, init methods ``` The **critical ordering fact**: the early-reference factory (step 2) can only be added *after* `createBeanInstance` (step 1) returns an object. You cannot expose a reference to something that hasn't been constructed. ## Field/setter cycle walk-through (A ↔ B) 1. Create A → constructor runs (no args needed) → A object exists. 2. `addSingletonFactory("a", …)` — A's early reference is now obtainable. 3. `populateBean(A)` needs B → create B. 4. B's constructor runs → `addSingletonFactory("b", …)`. 5. `populateBean(B)` needs A → `getSingleton("a")` finds A's early reference in the cache → B gets the half-built A. 6. B finishes (`initializeBean`) → B is a full singleton. 7. Back in A's `populateBean`, A gets the finished B → A finishes. The loop closes because at step 2 there was already an A object to expose. ## Constructor cycle walk-through (X ↔ Y) 1. Create X → `createBeanInstance(X)` must resolve constructor arg Y first. 2. Create Y → `createBeanInstance(Y)` must resolve constructor arg X first. 3. X is already marked in `singletonsCurrentlyInCreation` (a `Set` guarded early in `getSingleton`), and X is *not yet* in any cache because step 2 (`addSingletonFactory`) never ran for it — its constructor hasn't returned. 4. Spring sees X is 'currently in creation' but has no early reference available → throws **`BeanCurrentlyInCreationException`**. So the failure is structural: constructor injection needs a *finished-enough* dependency at instantiation time, but the early-exposure trick only becomes available *after* instantiation. ## Why the in-creation guard exists `DefaultSingletonBeanRegistry.beforeSingletonCreation` adds the bean name to `singletonsCurrentlyInCreation`. Re-entrancy on the same name without an available early reference is the definition of an unresolvable cycle, which is what triggers the exception. ## Practical implications - Prefer constructor injection generally (immutability, testability) — a cycle then surfaces immediately as a hard error, which is arguably *better* than silently resolving a bad design. - If a genuine constructor cycle is unavoidable, break it with `@Lazy` on one constructor parameter (injects a proxy so the real bean isn't needed at construction time), or refactor. ## Gotchas - Mixed cycles (A constructor-needs B, B field-needs A) can sometimes resolve depending on creation order, but this is fragile and order-dependent — don't rely on it. - `@Lazy` on the constructor parameter is the only injection-level way to make a pure constructor cycle work.
- Can @Lazy make a pure constructor cycle work?Yes. @Lazy on one constructor parameter injects a lazy proxy instead of the real bean, so the target isn't needed at construction time; the real bean is resolved on first method invocation, breaking the deadlock.
- Where is the 'currently in creation' state tracked?In DefaultSingletonBeanRegistry's singletonsCurrentlyInCreation set, managed by beforeSingletonCreation/afterSingletonCreation. Re-entering creation of the same bean with no available early reference triggers the exception.
saying these in an interview costs you the question
- Saying constructor cycles fail because of proxies (it's about instantiation ordering)
- Claiming addSingletonFactory happens before the constructor runs
- Suggesting allowCircularReferences=true fixes constructor cycles