How Spring Solves Circular Dependencies: Inside the Three‑Level Cache

This article provides a detailed walkthrough of Spring's circular‑dependency resolution, explaining the three‑level cache mechanism, step‑by‑step bean creation flow, and the underlying source‑code logic, complete with diagrams and code examples for deep understanding.

Architect's Guide
Architect's Guide
Architect's Guide
How Spring Solves Circular Dependencies: Inside the Three‑Level Cache

1. Basics

1.1 What Is a Circular Dependency?

A circular dependency occurs when two or more beans depend on each other directly or indirectly, forming a loop. Three typical scenarios exist, and a simple demo (Model1 ↔ Model2) illustrates the classic case.

@Service
public class Model1 {
    @Autowired
    private Model2 model2;
    public void test1() { }
}

@Service
public class Model2 {
    @Autowired
    private Model1 model1;
    public void test2() { }
}

This demo runs successfully; the following sections dissect Spring's internal execution.

1.2 Three‑Level Cache

Before diving into the code, understand Spring's three caches:

Level‑1 (singletonObjects): Stores fully initialized singleton beans.

Level‑2 (earlySingletonObjects): Holds early references to beans that have been instantiated but not yet fully populated.

Level‑3 (singletonFactories): Contains factories (often proxy factories) that can create bean instances on demand, crucial for breaking circular references.

1.3 Execution Flow Overview

The process can be visualized in three nested layers:

First layer: Spring attempts to retrieve bean A from Level‑1; not found, it creates a factory for A and stores it in Level‑3, then proceeds to create bean B.

Second layer: While creating B, Spring needs A again, so it looks up Level‑3, obtains A’s factory, creates a proxy (or early reference), and places it in Level‑2.

Third layer: Returning to the first layer, Spring now finds A’s early reference in Level‑2, completes A’s property injection, and finally moves A to Level‑1.

This three‑step “nesting doll” approach resolves the circular dependency.

2. Source‑Code Walkthrough

All examples were tested with Spring 5.2.15.RELEASE.

2.1 Entry Point

The demo starts with the Model1 bean; the code path begins at doGetBean(), which fails to find the bean in Level‑1 and proceeds to creation.

2.2 First Layer

Inside doGetBean(), Spring calls doCreateBean(), which registers a factory for Model1 in Level‑3 via addSingletonFactory(). The factory is stored in singletonFactories.

2.3 Second Layer

When creating Model2, Spring again calls doGetBean() and ultimately doResolveDependency() to locate Model1. It finds Model1’s factory in Level‑3, creates an early reference (proxy if AOP is involved), and stores it in Level‑2.

2.4 Third Layer

At this point, Level‑2 contains the early reference for Model1. Spring uses it to satisfy Model2’s dependency, completing Model2’s initialization.

2.5 Return to Second Layer

After Model2 is fully initialized, Spring invokes getSingleton(), which moves the bean from Level‑2 to Level‑1, clearing Level‑2.

2.6 Return to First Layer

Finally, Model1’s remaining properties are injected, and Model1 is promoted to Level‑1, ending the circular‑dependency resolution.

3. Deep Principle Analysis

3.1 Why a Three‑Level Cache?

Level‑1 guarantees Spring’s singleton semantics. Level‑3 stores factories to break cycles by providing early references before a bean is fully initialized. Level‑2 preserves those early references (often proxies) to avoid creating multiple proxy instances when AOP is applied.

3.2 Can Level‑2 Be Removed?

Consider beans A, B, C where A depends on B and C, and both B and C depend on A. If A is proxied (AOP), each lookup would create a new proxy (A1, A2) without Level‑2, breaking singleton guarantees. Level‑2 caches the early proxy, ensuring the same instance is reused.

@Service
public class A {
    @Autowired private B b;
    @Autowired private C c;
}

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

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

Thus, Level‑2 is essential when AOP creates distinct proxy objects; without it, multiple proxies would be generated, violating the singleton contract.

4. Summary

The three caches serve distinct purposes:

Level‑1 stores fully initialized singleton beans.

Level‑2 stores early references (often proxies) to resolve circular dependencies when AOP is present.

Level‑3 stores factories that can produce those early references, enabling the break of circular loops.

Understanding this mechanism equips you to debug Spring’s bean creation, recognize where circular dependencies are resolved, and appreciate the design choices behind the cache hierarchy.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaAOPspringDependency InjectionCircular DependencyThree-level Cache
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

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.