Why Spring Needs a Three‑Level Cache for Circular Dependencies (Two Levels Lose AOP Proxies)
Spring resolves singleton circular dependencies by using a three‑level cache—singletonObjects, earlySingletonObjects, and singletonFactories—to expose early bean references and generate AOP proxies on demand, avoiding the loss of proxies that occurs with a two‑level cache while preserving performance.
What is a circular dependency?
A circular dependency occurs when two or more beans reference each other, forming a loop. The classic example is:
@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 cycle
}If no special handling is applied, bean creation would dead‑lock: create A → need B → create B → need A → ...
Spring solves this by exposing the bean early during creation.
The three‑level cache
Spring’s DefaultSingletonBeanRegistry maintains three maps: singletonObjects – stores fully initialized singleton beans. earlySingletonObjects – stores early references (instantiated but not yet populated or initialized). singletonFactories – stores ObjectFactory instances that can create an early reference on demand.
The timing of each cache is:
After bean initialization completes, the bean is placed in singletonObjects.
During creation, before property injection, an ObjectFactory is added to singletonFactories.
When an early reference is needed, ObjectFactory.getObject() creates the reference, which is then moved to earlySingletonObjects and removed from singletonFactories.
Bean creation flow and cache usage
The core of AbstractAutowireCapableBeanFactory.doCreateBean() is:
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
// 1. Instantiate bean (constructor call)
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
Object bean = instanceWrapper.getWrappedInstance();
// 2. If singleton and circular references are allowed, expose ObjectFactory to third‑level cache
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
Object exposedObject = bean;
// 3. Populate properties (may trigger creation of dependent beans)
populateBean(beanName, mbd, instanceWrapper);
// 4. Initialize bean (init‑methods, BeanPostProcessors, etc.)
exposedObject = initializeBean(beanName, exposedObject, mbd);
// 5. Register final bean in first‑level cache, handling early reference if needed
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null && exposedObject == bean) {
exposedObject = earlySingletonReference;
}
}
return exposedObject;
}The crucial step is #2: after instantiation but before property injection, Spring stores an ObjectFactory in the third‑level cache. When another bean needs the early reference, getSingleton() looks first at the first‑level cache, then the second, and finally invokes the factory to obtain the early reference.
Why three levels are required
If only two levels ( singletonObjects + earlySingletonObjects) are used, the flow would place the raw bean instance directly into the second‑level cache. When a circular dependency is resolved, the dependent bean receives the original instance, not the AOP proxy that is created later during initialization. Consequently, AOP advice (transactions, logging, caching) is lost.
Two alternatives fail:
Generate the proxy immediately and store it in the second‑level cache – this forces every bean to create a proxy even when no circular dependency exists, wasting performance.
Delay proxy creation without a factory – the proxy would be created after initialization, again missing the early reference.
The three‑level design solves both problems: the factory defers proxy creation until it is actually needed, and the second‑level cache guarantees that the same early reference (proxy or raw bean) is reused, preserving singleton semantics.
Early bean reference and AOP proxy generation
When a circular dependency is detected, Spring calls getEarlyBeanReference() provided by AbstractAutoProxyCreator (or its subclasses). This method records that the bean has been proxied early and then invokes wrapIfNecessary() to create the proxy:
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
return wrapIfNecessary(bean, beanName, cacheKey);
}
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
return createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
}
return bean;
}Later, during normal post‑processing, postProcessAfterInitialization() checks earlyProxyReferences to avoid creating a second proxy.
Circular dependencies Spring cannot resolve
Constructor injection : beans are not fully instantiated before dependencies are needed, so early exposure is impossible. Spring throws BeanCurrentlyInCreationException.
Prototype‑scoped beans : each getBean() call creates a new instance, so there is no singleton cache to hold an early reference.
Special proxies (e.g., @Async) : the proxy creator does not implement SmartInstantiationAwareBeanPostProcessor, so an early reference cannot be generated and Spring reports an error.
Typical solutions are:
Switch to setter or field injection.
Annotate one side with @Lazy to defer actual bean creation.
Refactor the code to remove the circular dependency altogether.
Detecting and handling circular dependencies
Spring Boot 2.6+ disables circular references by default ( spring.main.allow-circular-references=false). Enabling them is only a temporary compatibility measure; the preferred approach is to eliminate the cycle.
Practical recommendations
Treat circular dependencies as a code smell; aim to refactor them out.
Use @Lazy or ApplicationContext lookup only as a last‑resort workaround.
Understand the three‑level cache mechanism to debug proxy‑related issues in 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.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
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.
