How Spring Solves Circular Dependencies: Deep 3‑Level Cache Walkthrough

This article provides an in‑depth walkthrough of how Spring resolves circular dependencies using its three‑level cache mechanism, complete with code examples, step‑by‑step execution flow, and a deep dive into why each cache level is essential.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
How Spring Solves Circular Dependencies: Deep 3‑Level Cache Walkthrough

1. Basics

1.1 What is circular dependency?

A circular dependency occurs when two or more beans depend on each other directly or indirectly, forming a loop. There are three typical scenarios. The article shows a simple demo representing scenario 2, where two @Service classes depend on each other:

@Service
public class Louzai1 {
    @Autowired
    private Louzai2 louzai2;
    public void test1() {}
}

@Service
public class Louzai2 {
    @Autowired
    private Louzai1 louzai1;
    public void test2() {}
}

This classic circular dependency can run, and the article will later dissect the full execution flow from the source code perspective.

1.2 Three‑level cache

Before diving into the source code, it is essential to understand Spring's three‑level cache logic:

First‑level cache (singletonObjects): stores fully initialized singleton bean instances.

Second‑level cache (earlySingletonObjects): stores bean instances that have been instantiated but not yet fully populated.

Third‑level cache (singletonFactories): stores factories that can create bean instances, mainly to break circular references.

The core idea is illustrated in the following diagram:

1.3 Execution principle

The article splits the execution of scenario 2 into six steps, resembling a nested "matryoshka" process:

First layer: Spring tries to retrieve bean A from the first‑level cache; not found, it creates a factory for A and puts it into the third‑level cache (A is a half‑finished bean).

Second layer: While creating A, Spring discovers that A depends on B, so it starts creating B.

Third layer: B depends on A, so Spring attempts to create A again. This time it finds A's factory in the third‑level cache, obtains a proxy (or early reference) for A, stores it in the second‑level cache, and removes the factory from the third‑level cache.

Returning to the second layer: B now receives the early reference of A, completes its own initialization.

Returning to the first layer: A receives the fully initialized B, finishes its own property injection and any AOP proxy creation, then moves to the first‑level cache.

Finally, A is placed into the first‑level cache, completing the circular dependency resolution.

The article emphasizes why the three‑level cache is necessary and promises to show the source code that implements this flow.

2. Source code analysis

Note: The Spring version used in the examples is 5.2.15.RELEASE . Different versions may have slight differences.

2.1 Code entry point

The debugging process starts from the bean name "louzai1" and follows the call chain through doGetBean(), createBean(), and related methods. The following screenshots illustrate the entry point and the initial steps:

2.2 First layer

Inside doGetBean(), Spring cannot find "louzai1" in the singleton cache, so it proceeds to create the bean. The process enters doCreateBean(), where addSingletonFactory() stores a factory for "louzai1" into the third‑level cache.

Next, populateBean() triggers postProcessProperties(), which selects a strategy object (shown in the diagram) to handle property injection.

The strategy ultimately resolves the dependency on "louzai2" by invoking doResolveDependency().

2.3 Second layer

Resolving "louzai2" follows the same path: doGetBean()doCreateBean()doResolveDependency(), which discovers that "louzai2" depends on "louzai1".

2.4 Third layer

When the third layer attempts to obtain "louzai1" again, the first‑level and second‑level caches are still empty, but the third‑level cache holds the factory for "louzai1". Spring uses this factory to create a proxy (or early reference) for "louzai1" and stores it in the second‑level cache.

2.5 Returning to second layer

With the early reference of "louzai1" now available, "louzai2" finishes its initialization. The second‑level cache still holds the proxy object.

2.6 Returning to first layer

After "louzai2" is fully initialized, Spring calls getSingleton() to move the bean from the second‑level cache to the first‑level cache, clearing the early reference.

The same process repeats for "louzai1", ultimately placing both beans into the first‑level cache.

3. Deep analysis

3.1 Why three‑level cache?

The first‑level cache (singletonObjects) guarantees Spring's singleton semantics. The third‑level cache (singletonFactories) stores factories that can produce early bean references, which is crucial for breaking circular references. The second‑level cache (earlySingletonObjects) holds the actual early references (which may be original objects or AOP proxies). Without the second‑level cache, each request for the early bean would invoke the factory again, potentially creating multiple proxy instances and breaking the singleton guarantee.

3.2 Can we drop the second‑level cache?

@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;
}

If A is subject to AOP, the factory in the third‑level cache would generate a new proxy each time B or C requests A, resulting in two distinct instances (A1 and A2). The second‑level cache stores the first proxy (A1) so subsequent requests retrieve the same instance, preserving the singleton property. Without AOP, only the first and third caches would suffice.

4. Conclusion

First‑level cache : the singleton pool for fully initialized beans.

Second‑level cache : holds early references, mainly to support AOP‑generated proxies.

Third‑level cache : stores factories that create early references, enabling circular‑dependency breaking.

Understanding these three caches gives a thorough grasp of how Spring resolves circular dependencies. The article encourages readers to debug the code themselves to solidify the concepts.

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.

aopspringcircular-dependencyThree-level CacheBeanFactory
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

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.