Backend Development 12 min read

How Spring Solves Circular Dependencies: Deep Dive into Three-Level Caches

This article explains why Spring’s circular dependency problem only occurs with singleton beans, how constructor versus setter injection affects resolution, and why Spring uses a three‑level cache (singletonObjects, earlySingletonObjects, singletonFactories) to expose incomplete beans and support proxy creation without breaking the bean lifecycle.

macrozheng
macrozheng
macrozheng
How Spring Solves Circular Dependencies: Deep Dive into Three-Level Caches

What Is a Circular Dependency?

Two beans that reference each other, e.g., class A autowires B and class B autowires A, create a circular dependency that can cause bean creation to stall.

<code>@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private A a;
}

// or a bean that depends on itself
@Service
public class A {
    @Autowired
    private A a;
}</code>

Spring resolves such cycles only for singleton beans and when not all injections are constructor‑based.

Why Must Beans Be Singletons?

Prototype‑scoped beans would cause infinite instantiation (A1 → B1 → A2 → B2 …), so Spring limits circular‑dependency resolution to singletons.

Why Constructor Injection Alone Fails

Bean creation follows three steps: 1) instantiate, 2) populate properties (setter injection), 3) initialize. If both A and B use only constructor injection, the container cannot obtain an incomplete instance from the early cache, so the cycle fails.

Mixed Constructor and Setter Injection

When one bean uses setter injection and the other uses constructor injection, the cycle can succeed because the partially created bean is stored in the early cache and later injected.

Full Process Spring Uses to Break Circular Dependencies

Spring maintains three caches for singleton beans:

SingletonObjects (first‑level) – fully created beans.

EarlySingletonObjects (second‑level) – beans that are instantiated but not yet populated.

SingletonFactories (third‑level) – factories that can create an early reference, allowing proxy creation.

When a bean is being created, Spring registers a factory in the third‑level cache:

<code>addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));</code>

During property injection, if a dependent bean is requested, the container checks the caches in order. If the bean is still in creation, the third‑level factory provides an early reference (potentially a proxy) which is placed into the second‑level cache, allowing the dependent bean to complete.

Why a Third‑Level Cache Is Needed

The third‑level cache enables Spring to expose a bean reference before full initialization, which is essential for creating AOP proxies without violating the bean lifecycle. Without this step, proxies would be created too early.

Conclusion

Spring solves circular dependencies by exposing incomplete singleton beans through a three‑level cache, allowing setter injection to succeed and enabling proxy creation at the correct lifecycle stage. Constructor‑only injection still fails, and the bean name’s alphabetical order can affect resolution.

Springdependency injectionCircular DependencysingletonThree-level Cache
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.