skip to content

What is a circular dependency between Spring beans, and how does Spring's handling differ between setter/field injection and constructor injection?

level: juniorimportance: must knowfreq 70%

answer

  1. A→B→A loop
  2. constructor = fails, setter/field = resolvable
  3. instantiate then populate (two phases)
  4. early reference of half-built bean
  5. Boot 2.6 default false

basics

~20 s

A circular dependency is when bean A needs B and B needs A. Spring can resolve this for setter/field injection (it injects a half-built A into B), but constructor injection fails because neither bean can be built first.

solid answer

~40 s

A circular dependency exists when two (or more) beans reference each other, e.g. A depends on B and B depends on A. With setter or field injection Spring can break the cycle: it constructs A (empty), exposes a reference to that half-initialized A, then constructs B and injects that early A reference, finishes B, and finally finishes wiring A. With constructor injection this is impossible — A's constructor needs a finished B and B's constructor needs a finished A, so neither can be instantiated first. Spring detects this and throws BeanCurrentlyInCreationException at startup. Since Spring Boot 2.6 even the resolvable (setter/field) case is rejected by default. The clean fix is to redesign; quick fixes are @Lazy on one injection point or (for setter/field) re-enabling allowCircularReferences.

code

java · 20 lines
java
// Setter/field cycle — historically resolvable
@Component
class A {
    @Autowired B b;   // field injection: set during populate phase
}

@Component
class B {
    @Autowired A a;   // receives the early, half-built A
}

// Constructor cycle — ALWAYS fails: BeanCurrentlyInCreationException
@Component
class X {
    X(Y y) {}         // needs finished Y first
}
@Component
class Y {
    Y(X x) {}         // needs finished X first — deadlock
}

go deeper

for a junior

Know the definition and that setter/field cycles can be resolved but constructor cycles cannot.

for a middle

Explain the instantiate-then-populate phases and why that lets setter/field cycles work.

for a senior

Tie the resolution to early singleton exposure and mention the Boot 2.6 default change.

for a principal

Discuss when a cycle indicates an architectural flaw and how to design it away vs. patch it.

## What is a circular dependency? A **circular (cyclic) dependency** occurs when beans depend on each other directly or transitively, forming a loop: `A → B → A`, or longer chains like `A → B → C → A`. Because Spring must fully create a bean's dependencies before (or during) creating the bean, a cycle raises a chicken-and-egg problem. ## Why injection style matters Spring instantiates a singleton bean in two phases inside `AbstractAutowireCapableBeanFactory`: 1. **Instantiation** — call the constructor to create the raw object. 2. **Population** — inject fields / call setters (`populateBean`) and run init callbacks (`initializeBean`). - **Constructor injection**: the dependency must be supplied *at step 1*. To build `A` you already need a finished `B`, and to build `B` you need a finished `A`. Nothing can be instantiated first, so Spring cannot break the loop. It throws `BeanCurrentlyInCreationException`. - **Setter / field injection**: the dependency is supplied *at step 2*. Spring can finish step 1 for `A` (a bare object exists) and then **expose that half-built `A`** to whoever needs it. So when `B` is being populated and asks for `A`, it receives the early, not-yet-fully-initialized `A` reference. `B` completes, then `A`'s population completes using the finished `B`. ## The mechanism: early singleton exposure Spring uses a **three-level cache** in `DefaultSingletonBeanRegistry` to hand out early references. Right after instantiating `A` (but before populating it), Spring registers an `ObjectFactory` for `A` (`addSingletonFactory`). If something asks for `A` mid-creation, that factory yields an *early reference*. This only works because the reference exists after construction — which is exactly why it fails for constructor cycles (no object exists yet to expose). ## Default behavior changed in Spring Boot 2.6 Historically Spring silently resolved setter/field cycles. Since **Spring Boot 2.6 / Spring Framework 5.3**, `spring.main.allow-circular-references` defaults to **false**, so even resolvable cycles fail fast at startup with a clear error unless you opt back in. The guidance is to treat a cycle as a design smell. ## Fixes - **Redesign** — extract the shared logic into a third bean, or use an event, so the loop disappears (preferred). - **`@Lazy`** on one injection point — injects a lazy proxy, deferring real resolution until first use; works even for constructors. - **Setter/field injection** instead of constructor for one side. - **Re-enable** `spring.main.allow-circular-references=true` (setter/field cycles only) — a band-aid. ## Gotchas - Constructor cycles are **never** auto-resolvable, regardless of the flag. - `@Async`, AOP proxies, or `@Scope`d beans can complicate early exposure (the early reference may need to be a proxy). - The error `BeanCurrentlyInCreationException` and `Requested bean is currently in creation: Is there an unresolvable circular reference?` are the tell-tale messages.

  • Which exception does Spring throw for an unresolvable constructor cycle, and when?
    BeanCurrentlyInCreationException, thrown eagerly at application startup (context refresh) when it tries to instantiate the beans, with a message like 'Is there an unresolvable circular reference?'.
  • Is a circular dependency ever a good design?
    Rarely. It usually signals a missing abstraction or misassigned responsibility. Preferred fixes are extracting a third collaborator, inverting a dependency, or publishing an event rather than relying on Spring to untangle it.

saying these in an interview costs you the question

  • Claiming Spring can resolve constructor-injection cycles
  • Thinking @Autowired on a field and on a constructor behave the same for cycles
  • Believing cycles are still silently allowed by default in modern Spring Boot

context