How Spring Solves Circular Dependencies with a Three‑Level Cache
This article explains how Spring's three‑level cache (singleton, early‑singleton, and singleton‑factory caches) works together with bean post‑processors to break circular dependencies and correctly create AOP proxies, while also showing how to disable circular references when needed.
1. Circular Dependency
In Spring, a bean that depends on another bean which in turn depends on the first creates a circular dependency. Spring resolves this by using a three‑level cache, which are essentially three Map objects.
<code>@Component
public class A {
@Resource
private B b;
}
@Component
public class B {
@Resource
private A a;
}</code>The caches are defined in DefaultSingletonBeanRegistry :
<code>public class DefaultSingletonBeanRegistry {
// Level 1: fully initialized singletons
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// Level 3: factories that create early references (proxies)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
// Level 2: early singleton objects (partially initialized)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
}</code>1.1 Bean Retrieval Process
When doGetBean is called, Spring first checks the level‑1 cache. If the bean is not there and is currently being created, it looks in the early‑singleton cache (level 2). If still missing and early references are allowed, it obtains an ObjectFactory from the level‑3 cache, creates the proxy, stores it in level 2, and removes the factory from level 3.
<code>protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}</code>1.2 Dependency Injection Steps
Create instance of A and put it into earlySingletonObjects (level 2).
Populate A's properties; discover dependency on B.
Create instance of B and put it into earlySingletonObjects .
Populate B's properties; discover dependency on A.
Resolve A from earlySingletonObjects and inject into B.
Continue B's initialization, possibly creating an AOP proxy via addSingletonFactory .
After B is fully initialized, move it to singletonObjects (level 1) and clear level 2/3 entries.
Inject the now‑ready B (or its proxy) back into A and finish A's initialization.
If both A and B need AOP proxies, the three‑level cache ensures that the proxy is created early (in level 3) and the same proxy instance is used for both beans, avoiding the situation where one bean receives a raw instance while the other receives a proxy.
1.3 How the Three‑Level Cache Solves the Problem
Without level 3, Spring would have to create a proxy before it knows whether a bean actually needs one, leading to unnecessary proxy creation. With the ObjectFactory in level 3, the proxy is created lazily only when an early reference is required.
1.4 Disabling Circular References
Developers can turn off Spring's circular‑reference support by setting allowCircularReferences to false in a custom BeanFactoryPostProcessor :
<code>@Component
public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof AbstractAutowireCapableBeanFactory) {
((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(false);
}
}
}</code>When disabled, Spring will throw an exception if a circular dependency is detected.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
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.