skip to content

How does @Lazy break a circular dependency, and why does it work even for constructor injection where the three-level cache cannot?

level: seniorimportance: should knowfreq 55%

answer

  1. @Lazy injects a proxy, not the real bean
  2. proxy has no dep at construction time
  3. real bean resolved on first method call
  4. works for constructor cycles too
  5. put on ONE side; buildLazyResolutionProxy

basics

~20 s

@Lazy on an injection point makes Spring inject a lazy proxy instead of the real bean. The proxy is created immediately with no dependency on the target, so construction succeeds; the real bean is looked up only when a method is first called, by which time both beans exist.

solid answer

~40 s

Putting @Lazy on one injection point in the cycle tells Spring to inject a lazy-initialization proxy rather than the actual bean. The proxy (produced via ContextAnnotationAutowireCandidateResolver.buildLazyResolutionProxy, a CGLIB/JDK proxy) is a lightweight stand-in that holds no real reference to the target at construction time — so the constructor or setter completes without needing the other bean to exist yet. On the first actual method call, the proxy resolves the real bean from the context (which by then is fully built) and delegates. This works even for constructor injection, where the three-level cache is useless: the cache needs a constructed object to expose, but @Lazy sidesteps the whole problem by never requiring the real target during construction. Place @Lazy on just one side of the cycle to break it; the other side can inject normally.

code

java · 18 lines
java
@Component
class A {
    private final B b;
    // @Lazy => Spring injects a proxy of B; A's constructor completes
    // without a real B existing yet.
    A(@Lazy B b) { this.b = b; }

    void doWork() {
        b.help(); // first call: proxy resolves the real, fully-built B
    }
}

@Component
class B {
    private final A a;
    B(A a) { this.a = a; } // gets the finished A normally
    void help() {}
}

go deeper

for a junior

Know @Lazy injects a proxy that defers resolving the real bean until first use.

for a middle

Explain that this lets construction complete without the real target, breaking the cycle.

for a senior

Explain why it works for constructor cycles where the three-level cache can't, and correct placement.

for a principal

Discuss proxy overhead, JDK vs CGLIB, side-effect timing, and @Lazy as a smell vs. redesign.

## What @Lazy does at an injection point `@Lazy` on a field, setter, or constructor parameter (not on the bean class itself) changes *what gets injected*: instead of the real bean, Spring injects a **lazy-resolution proxy**. This is built by `ContextAnnotationAutowireCandidateResolver.getLazyResolutionProxyIfNecessary` / `buildLazyResolutionProxy`, which creates a proxy whose `TargetSource` resolves the actual bean from the `BeanFactory` **on each invocation** (via `doResolveDependency`). Key property: **the proxy is constructed with no dependency on the target bean**. It only needs the `BeanFactory` and the dependency descriptor. So the enclosing bean's constructor/setter can complete immediately. ## Why it breaks constructor cycles Recall the constructor-cycle deadlock: to build X you need a finished Y, to build Y you need a finished X. Add `@Lazy` to X's constructor parameter: 1. Create X → constructor needs a `Y`. Spring supplies a **lazy proxy of Y** (no real Y needed). X finishes. 2. Create Y → constructor needs an `X`. X is now a full singleton (or resolvable), so Y gets it. Y finishes. 3. Later, X calls a method on its `Y` proxy → the proxy resolves the real, finished Y from the context and delegates. The three-level cache couldn't help here because there was never a constructed object to expose early. `@Lazy` avoids the problem entirely by **decoupling construction from real resolution**. ## Placement Put `@Lazy` on **one** injection point in the cycle — the minimum needed to break it. Adding it to both works but is unnecessary. Choose the side where deferred resolution is semantically harmless (the bean isn't used during the other's construction/init). ## Trade-offs and gotchas - **Proxy overhead**: every call goes through the proxy's target-source resolution (a bean-factory lookup, cheap but non-zero). - **Type**: for interfaces you get a JDK dynamic proxy; for classes a CGLIB subclass (final classes/methods can't be proxied → failure). - **Not eager-init safe for actual first-use timing**: if the target's initialization has side effects you expected at startup, they now happen on first call. - **Still a design smell**: `@Lazy` hides a cycle rather than removing it. Prefer refactoring (extract a third bean, publish an event) when practical. - `@Lazy` on the **bean class/definition** is different — it defers the whole bean's creation until first use; that can *also* incidentally break some cycles but has broader effects. The targeted, injection-point `@Lazy` is the precise tool. ## Relationship to allowCircularReferences `@Lazy` works regardless of the `spring.main.allow-circular-references` flag and regardless of injection style, which is why it's the recommended quick fix in modern Spring Boot where the flag defaults to false.

  • Does @Lazy on the injection point create the proxy eagerly or the target eagerly?
    The proxy is created eagerly (at injection), but it holds no real target. The actual target bean is resolved lazily from the BeanFactory on the first method invocation through the proxy.
  • Where should you put @Lazy in a two-bean cycle?
    On just one injection point — the minimum to break the loop. Both would work but is redundant. Pick the side whose dependency isn't needed during the other bean's construction or init.

saying these in an interview costs you the question

  • Saying @Lazy injects null until first use (it injects a proxy, not null)
  • Claiming @Lazy cannot help constructor cycles
  • Confusing injection-point @Lazy with class-level @Lazy (defer whole bean)

context