Spring resolves singleton Bean circular dependencies in field or setter injection through a three-level cache. The core idea is to expose an early reference in advance while deferring the decision of whether to create a proxy object. This mechanism prevents IoC instantiation deadlocks and preserves AOP proxy consistency. Keywords: Spring circular dependencies, three-level cache, AOP proxy.
Technical Specifications Snapshot
| Parameter | Description |
|---|---|
| Core Framework | Spring / Spring Boot |
| Implementation Language | Java |
| Key Mechanisms | IoC, Dependency Injection, AOP, Singleton Registry |
| Core Class | DefaultSingletonBeanRegistry |
| Key Caches | singletonObjects, earlySingletonObjects, singletonFactories |
| Applicable Scope | Singleton Beans, field injection, or setter injection |
| Not Applicable | Constructor circular dependencies, Prototype Beans |
| Source / License | Originally published as a CSDN technical article, CC 4.0 BY-SA |
| Star Count | Not provided in the original content |
| Core Dependencies | Spring Beans, Spring Context, AOP module |
Circular dependencies are fundamentally mutual waiting during Bean creation
A typical circular dependency looks like this: A depends on B, and B depends on A. When Spring creates A, it first instantiates the object and then populates its properties. If property population requires B, Spring switches to creating B. The problem is that while populating B, Spring needs A again, but A has not finished initialization yet.
@Service
public class A {
@Autowired
private B b; // A depends on B
}
@Service
public class B {
@Autowired
private A a; // B depends on A, forming a circular dependency
}
This code shows the most common circular dependency scenario with field injection.
Spring does not handle every kind of circular dependency
Spring can resolve only some circular dependencies. More precisely, it can resolve circular dependencies between singleton Beans that use field injection or setter injection. In these cases, Spring can instantiate the object as an “empty shell” first and fill in dependencies later.
If you use constructor injection, the problem becomes: “the object cannot even be constructed.” In that case, there is no early reference to expose, so Spring cannot break the cycle.
@Service
public class A {
private final B b;
public A(B b) { // B is required during construction
this.b = b;
}
}
This kind of code gets stuck before instantiation, and the three-level cache cannot help.
The three-level cache is designed to expose early references safely during the lifecycle
Spring maintains a three-level cache in DefaultSingletonBeanRegistry. These caches are not just performance optimizations. They are a precise design that supports the Bean lifecycle and the timing of proxy creation.
| Cache Level | Field Name | Stored Content | Purpose |
|---|---|---|---|
| Level 1 Cache | singletonObjects |
Fully initialized Beans | Provides the final singleton object externally |
| Level 2 Cache | earlySingletonObjects |
Early exposed Bean references | Prevents repeated creation of early references |
| Level 3 Cache | singletonFactories |
ObjectFactory<?> factories |
Defers the decision to return the raw object or a proxy |
The three-level cache solves when to expose and what to expose
The first-level cache contains only finished objects, so it cannot break a circular dependency. The second-level cache can store half-finished objects, but if Spring stores an object there too early, it loses control over AOP proxy timing.
That is why the third-level cache stores not the object itself, but a factory. Only when someone actually requests the early reference does Spring execute the factory logic and decide whether to return the raw object or a proxy.
The mutual dependency between A and B passes through the three-level cache in a fixed order
The following walkthrough reconstructs the full process for the case where A depends on B and B depends on A.
The first step is to instantiate A and put its early-reference factory into the third-level cache
When A is created, Spring first instantiates it. At this point, A is still an empty object: no properties injected and no initialization completed. Spring then places an ObjectFactory that can generate A’s early reference into the third-level cache.
AI Visual Insight: The diagram shows the container state right after A finishes instantiation. The first-level and second-level caches are empty, while the third-level cache now contains a factory pointing to A’s early reference. This shows that Spring does not expose the object itself immediately. Instead, it first registers a lazily executable reference factory.
The second step is that A triggers the creation of B during property population
When Spring injects properties into A, it detects that B is required, so it enters B’s creation flow. At this point, A is still marked as “currently in creation.”
AI Visual Insight: This diagram shows that after A enters dependency resolution, the container switches control flow to B’s creation process. The technical point is that A has not entered the first-level cache yet. Its context exists only in the “currently in creation” set and the third-level cache.
The third step is to instantiate B and create a factory for B as well
Spring handles B the same way it handles A: instantiate first, then write B’s early-reference factory into the third-level cache for later dependency resolution.
AI Visual Insight: The figure shows that the container now holds the creation context for both A and B. The third-level cache contains at least B’s factory, and some implementation explanations also keep A’s factory at this point. This reflects Spring’s phased management of unfinished Beans.
// Simplified view: expose the factory early after instantiation
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
The meaning of this logic is simple: create the object first, and postpone the proxy decision until an early reference is actually needed.
The fourth step is that B detects its dependency on A and retrieves A’s early reference from the third-level cache
While injecting properties into B, Spring needs A, so it calls getSingleton("a"). The lookup order is level 1 cache, level 2 cache, then level 3 cache. Since A has not completed initialization yet, the first two levels usually miss, and Spring hits the third-level cache.
AI Visual Insight: This diagram highlights the critical moment when the third-level cache is hit: the container executes ObjectFactory.getObject() to generate A’s early reference. If A requires AOP, the returned object at this point may already be a proxy rather than the raw object.
After the hit, three things happen: Spring executes the factory, moves the result into the second-level cache, and removes the factory from the third-level cache. This guarantees that the early reference is created only once.
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName); // Check level 1 cache first
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName); // Then check level 2 cache
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> factory = this.singletonFactories.get(beanName); // Finally check level 3 cache
if (factory != null) {
singletonObject = factory.getObject(); // Create the early reference, possibly a proxy
this.earlySingletonObjects.put(beanName, singletonObject); // Store it in level 2 cache
this.singletonFactories.remove(beanName); // Remove the factory from level 3 cache
}
}
}
return singletonObject;
}
This source code shows the core transition in the three-level cache: the factory executes only once, and its result settles into the second-level cache.
The fifth step is that B completes initialization first, and then A resumes and completes
After B gets A’s early reference, B can finish property injection and initialization. B then enters the first-level cache as a completed Bean. Control then returns to A, which injects the fully initialized B, and finally A also enters the first-level cache.
AI Visual Insight: The figure shows that B has completed initialization and entered the first-level cache, while A is still pending completion. This stage demonstrates the container scheduling strategy: unblock the depended-on side first, then backfill the original creation chain.
AI Visual Insight: This figure shows that after A obtains the completed B, it continues property population and initialization. Temporary state in the second-level and third-level caches starts to be cleaned up, and the Bean lifecycle gradually converges.
AI Visual Insight: The final diagram shows that both Beans have entered the first-level cache, and the container no longer keeps intermediate references. Technically, this means singleton semantics, dependency integrity, and proxy consistency have all been achieved.
The third-level cache exists because the second-level cache alone cannot support delayed AOP proxying
The conclusion is clear: a two-level cache is not entirely incapable of resolving circular dependencies, but it cannot elegantly satisfy Spring’s lifecycle design.
With only level 1 and level 2 caches, proxy creation would be forced too early
Under normal conditions, AOP proxy creation should happen after initialization, through BeanPostProcessor, especially during postProcessAfterInitialization. If Spring had only a second-level cache, then to resolve circular dependencies it would have to decide immediately after instantiation whether the Bean needs a proxy and place that result into the cache in advance.
This leads to two problems. First, every Bean would be forced into an early proxy decision. Second, the lifecycle order would be disrupted, violating Spring’s extension model.
With only level 1 and level 3 caches, singleton semantics could no longer be guaranteed reliably
Another flawed design would keep the factory but remove the second-level cache. In that case, every request for an early reference could execute the factory again. If proxy creation is involved inside the factory, Spring could produce multiple distinct references, breaking singleton consistency.
So the second-level cache is not just “one more storage layer.” It caches the single result produced by the third-level factory.
// Value of the level 2 cache: create the same early reference only once
Object earlyRef = singletonFactories.get(beanName).getObject(); // Execute the factory the first time
earlySingletonObjects.put(beanName, earlyRef); // Freeze the unique result
singletonFactories.remove(beanName); // Do not generate it again later
This logic ensures that early object references and final singleton semantics do not diverge into multiple instances.
The easiest interview mistake is saying Spring can solve circular dependencies without stating the boundaries
You should remember three limits clearly.
First, Spring resolves field/setter circular dependencies between singleton Beans. Second, constructor circular dependencies cannot be resolved. Third, the three-level cache is not only for circular dependencies; it also exists to support delayed exposure for AOP proxies.
FAQ
Q1: Why can’t Spring resolve circular dependencies with constructor injection?
A1: Because constructor injection requires complete dependencies before instantiation. In a circular chain, those objects have not been created yet, so there is no early reference to expose, and the three-level cache cannot intervene.
Q2: If the second-level cache can already store half-finished Beans, why is the third-level cache still necessary?
A2: Because the second-level cache can store only a result, not a deferred decision. The third-level cache stores an ObjectFactory, which allows Spring to decide at the moment of an actual circular dependency whether to return the raw object or an AOP proxy.
Q3: Does the third-level cache mean Spring can resolve all circular dependencies in AOP scenarios?
A3: No. It mainly guarantees early proxy exposure for singleton field-injection scenarios. Once constructor injection, Prototype Beans, or special proxy chains are involved, resolution can still fail, and you usually need to refactor the dependency relationship.
Core summary: This article systematically explains how Spring uses its three-level cache to resolve singleton Bean circular dependencies in field injection, and why a two-level cache is not enough to support both AOP proxies and the full Bean lifecycle. It includes the cache structure, creation flow, source code details, diagram-based explanations, and common interview questions.