How Spring Resolves Circular Dependencies – In‑Depth Interview Guide
This article walks through Spring's three‑level cache mechanism and source‑code flow that break circular dependencies, illustrating each step with a concrete demo, explaining the role of AOP, and showing why each cache level is necessary.
What is a circular dependency
A circular dependency occurs when two or more beans depend on each other directly or indirectly, forming a loop. The article demonstrates a classic case with two services:
@Service
public class Louzai1 {
@Autowired
private Louzai2 louzai2;
public void test1() {}
}
@Service
public class Louzai2 {
@Autowired
private Louzai1 louzai1;
public void test2() {}
}Although this example runs, Spring resolves the loop through a three‑level cache mechanism.
Three‑level cache in Spring
First‑level cache ( singletonObjects): stores fully created, injected and initialized singleton beans.
Second‑level cache ( earlySingletonObjects): stores bean instances that have been instantiated but are not yet fully initialized (early references).
Third‑level cache ( singletonFactories): stores ObjectFactory objects that can create a bean’s proxy or raw instance, enabling the break of circular references.
Lookup order: first‑level → second‑level → third‑level. When an entry is found in the third‑level, the factory is invoked, the resulting object is moved to the second‑level, and the factory entry is removed.
Execution process (layered walkthrough)
First layer: request bean Louzai1. It is absent from all caches, so creation starts. A factory for Louzai1 is placed into the third‑level cache while its properties remain unset.
Second layer: creating bean Louzai2 is triggered because Louzai1 depends on it. Louzai2 again depends on Louzai1, causing recursion.
Third layer: creation of Louzai1 is attempted again. The factory from the third‑level cache is invoked, producing a proxy (or raw instance) which is stored in the second‑level cache and the factory entry is cleared.
Return to second layer: the proxy satisfies Louzai2 ’s dependency, allowing Louzai2 to finish initialization.
Return to first layer: with Louzai2 fully initialized, property injection into Louzai1 completes, followed by AOP weaving and full initialization.
Finally, Louzai1 is moved into the first‑level cache.
Source‑code walk‑through (Spring 5.2.15.RELEASE)
Entry point
Bean retrieval starts at doGetBean(). The bean name is filtered until louzai1 is reached, then the call chain proceeds.
First layer
doGetBean()does not find louzai1 in singletonObjects, so it calls doCreateBean(). doCreateBean() invokes addSingletonFactory(), placing a factory for louzai1 into singletonFactories.
Next, populateBean() and postProcessProperties() run; the dependency resolver ( doResolveDependency()) discovers that louzai1 needs louzai2, triggering the second layer.
Second layer
The same sequence repeats for louzai2: doGetBean() → doCreateBean() → addSingletonFactory() → populateBean().
The resolver finds that louzai2 depends on louzai1, leading to the third layer.
Third layer
The third‑level cache already contains the factory for louzai1. Spring calls the factory, obtains the proxy (or raw instance), stores it in earlySingletonObjects, and removes the factory from singletonFactories. This early reference satisfies louzai2 ’s dependency.
Returning to second layer
With the early reference in place, louzai2 finishes initialization. getSingleton() later moves the bean from the second‑level cache into the first‑level cache.
Returning to first layer
Finally, louzai1 receives the fully initialized louzai2, completes its own property injection, AOP weaving, and is placed into singletonObjects.
Why three caches
The first‑level cache guarantees Spring’s singleton semantics. The third‑level cache stores factories that can create a proxy, which is essential for breaking circular references. The second‑level cache holds the early reference (proxy or raw bean) so that repeated look‑ups reuse the same instance, preventing multiple proxy creations when AOP is involved.
Can the second‑level cache be removed?
Consider three beans where A depends on B and C, and both B and C depend on A:
@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, each call to the factory in the third‑level cache would create a new proxy instance. Without the second‑level cache, the following would happen: B obtains a proxy A1 from the factory. C obtains a separate proxy A2 from the same factory.
Two distinct proxy objects break the singleton contract. The second‑level cache stores the first proxy ( A1) so that subsequent look‑ups return the same instance, preserving singleton semantics. If no AOP is applied, the second‑level cache is not strictly required.
Take‑aways
First‑level cache ( singletonObjects) stores fully initialized singleton beans and enforces Spring’s singleton guarantee.
Third‑level cache ( singletonFactories) stores factories that can produce an early reference (proxy or raw bean) to break circular dependencies.
Second‑level cache ( earlySingletonObjects) caches the early reference, ensuring that AOP proxies are created only once and that the same instance is injected wherever needed.
Understanding the three‑level cache and the step‑by‑step bean creation flow is essential for mastering Spring’s circular‑dependency handling and for debugging complex bean graphs.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
LouZai
10 years of front‑line experience at leading firms (Xiaomi, Baidu, Meituan) in development, architecture, and management; discusses technology and life.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
