skip to content

How does the injection style you choose affect circular dependencies and proxy-based features like @Transactional?

level: principalimportance: nice to knowfreq 35%

answer

  1. ctor cycle -> BeanCurrentlyInCreationException (fail fast)
  2. setter/field can resolve cycle via early reference
  3. @Lazy proxy breaks ctor cycle
  4. Boot 2.6+ allow-circular-references=false default
  5. proxy injected at all styles; self-invocation bypasses it

basics

~20 s

Constructor injection fails fast on circular dependencies (BeanCurrentlyInCreationException), forcing a redesign. Setter/field injection can resolve cycles by wiring after construction. Proxying (@Transactional) works with all styles because Spring injects the proxy, not the raw bean.

solid answer

~50 s

Injection style interacts with two container mechanics. First, circular dependencies: with pure constructor injection Spring can't finish building either bean — each needs the other fully constructed — so it throws BeanCurrentlyInCreationException at startup. That's a feature: it forces you to break the cycle by redesigning or, if unavoidable, by making one side setter/field-injected or @Lazy, which lets Spring inject a proxy or complete the object later. Second, AOP proxying for @Transactional, @Async, @Cacheable: Spring wraps the bean in a proxy during post-processing and injects the proxy at every injection point, so all three styles receive the advised proxy correctly. The classic pitfall is orthogonal to injection style — self-invocation, where a bean calls its own @Transactional method internally, bypasses the proxy. Since Spring 2.6, circular references are prohibited by default (spring.main.allow-circular-references=false), nudging everyone toward constructor injection and cycle-free design.

code

java · 21 lines
java
// Constructor cycle -> startup failure (desirable). Break it with @Lazy if truly needed:
@Service
class A {
    private final B b;
    A(@Lazy B b) { this.b = b; } // injects a lazy proxy, breaking the construction cycle
}
@Service
class B {
    private final A a;
    B(A a) { this.a = a; }
}

// Self-invocation pitfall (orthogonal to injection style):
@Service
class BillingService {
    public void run() {
        charge(); // calls this.charge() directly -> @Transactional NOT applied!
    }
    @Transactional
    public void charge() { /* ... */ }
}

go deeper

for a junior

Unlikely to know cycle/proxy interactions; may just know @Transactional exists.

for a middle

Knows constructor cycles fail and proxies exist, without the mechanism.

for a senior

Explains early-reference cycle resolution and self-invocation bypass.

for a principal

Reasons about keeping the bean graph acyclic, the Boot 2.6 default, and @Lazy as a deliberate escape hatch.

**Circular dependencies.** A **circular (cyclic) dependency** is when bean A needs B and B needs A (directly or transitively). - **Pure constructor injection**: unresolvable. To construct A, Spring must first have B; to construct B, it must first have A. Spring detects this and throws `BeanCurrentlyInCreationException` (wrapped in `UnsatisfiedDependencyException`) at startup. This **fail-fast** behavior is desirable — a cycle is almost always a design smell (two classes too tightly coupled) that should be broken by extracting a third collaborator or an event. - **Setter/field injection**: Spring can resolve some cycles because it instantiates both beans first (via their no-arg/constructor phase) and *then* populates the setter/field, using an early **bean reference** exposed from its singleton creation cache (the "early singleton exposure" via `getEarlyBeanReference`). This silently papers over the cycle. - **@Lazy**: annotating a constructor parameter `@Lazy` makes Spring inject a **lazy-resolving proxy** instead of the real bean, deferring resolution and thus breaking the construction cycle even with constructor injection. - **Spring Boot 2.6+**: `spring.main.allow-circular-references` defaults to **false** — cycles now fail the context by default regardless of style, a deliberate push toward cycle-free design and constructor injection. **Proxying and AOP.** Features like `@Transactional`, `@Async`, `@Cacheable`, and `@PreAuthorize` are implemented with **proxies** (JDK dynamic proxies for interfaces, CGLIB subclass proxies otherwise) created by a `BeanPostProcessor` after the bean is instantiated. Crucially, Spring registers the **proxy** as the bean, so **every injection point — constructor, setter, or field — receives the proxy**, not the raw target. So injection style does not break proxying. Two real pitfalls, both **orthogonal to injection style**: - **Self-invocation**: when a method inside the bean calls another `@Transactional`/`@Async` method on `this`, the call goes to the raw object and bypasses the proxy, so the advice (transaction, async) does **not** apply. Fix by refactoring the call to go through the injected proxy (self-inject via `ObjectProvider`/`@Lazy self`), or restructure into two beans. - **final classes/methods with CGLIB**: CGLIB can't subclass a `final` class or override `final` methods, so a proxy can't be created — but note constructor-injected beans are fine; it's the *target* being final that matters, and Kotlin classes are final by default (hence `kotlin-spring`/all-open plugin opens `@Transactional` classes). **Design-level takeaway (principal lens).** Prefer constructor injection precisely *because* it turns hidden cycles into loud startup failures, keeping the dependency graph a DAG. Reserve `@Lazy`/setter injection for the rare, deliberately-accepted cycle. Understand that proxying is a separate axis — injection style never changes whether you get the proxy, but self-invocation always defeats it. **Key APIs/terms.** `BeanCurrentlyInCreationException`, `UnsatisfiedDependencyException`, `@Lazy`, `spring.main.allow-circular-references`, JDK dynamic proxy vs CGLIB, `BeanPostProcessor`, self-invocation, early singleton exposure.

  • Why does self-invocation of a @Transactional method skip the transaction, and how is it unrelated to injection style?
    The transaction is applied by a proxy wrapping the bean; a call to this.method() targets the raw object and never passes through the proxy, so no advice runs. It's unrelated to injection style because every injection style receives the same proxy — the bypass happens on internal this-calls, not on how the bean was wired.
  • What changed in Spring Boot 2.6 regarding circular references?
    spring.main.allow-circular-references defaults to false, so circular dependencies now fail the application context at startup unless explicitly re-enabled — encouraging constructor injection and acyclic design.
  • How can you break a genuine constructor-injection cycle without abandoning constructor injection everywhere?
    Annotate one constructor parameter with @Lazy so Spring injects a lazy proxy, deferring the real bean resolution until first use; better still, redesign to remove the cycle (extract a shared collaborator or use an event).

saying these in an interview costs you the question

  • Claiming @Transactional doesn't work with constructor injection
  • Thinking field injection is required to make a bean proxyable
  • Believing self-invocation problems are caused by the injection style rather than proxying
  • Assuming circular dependencies are always fine because setter injection resolves them

context