Backend Development 14 min read

How Spring Resolves Circular Dependencies Using Early Exposure and a Three‑Level Cache

This article explains Spring's circular‑dependency solution, detailing the early‑exposure mechanism, the three‑level cache (singletonObjects, earlySingletonObjects, singletonFactories), how BeanPostProcessors and AOP proxies interact during bean creation, and why the third‑level cache is essential for correct bean wiring.

Top Architect
Top Architect
Top Architect
How Spring Resolves Circular Dependencies Using Early Exposure and a Three‑Level Cache

Preface

Hello, I am a senior architect.

Q: How does Spring solve circular dependencies?

A: Spring uses an early‑exposure mechanism backed by a three‑level cache.

Q: If early exposure to the second‑level cache can already solve the problem, why is a third‑level cache needed?

The answer lies in how Spring integrates BeanPostProcessors (especially AOP) with the early‑exposed bean reference.

Source Code Analysis

In the core bean‑creation method doGetBean , before a bean is instantiated Spring first attempts to retrieve it from the three‑level cache, which is the entry point for circular‑dependency handling.

(1) Retrieving a bean from the cache

protected
T doGetBean(final String name, @Nullable final Class
requiredType,
        @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
    final String beanName = transformedBeanName(name);
    Object bean;
    // 2. Try to get bean from cache
    Object sharedInstance = getSingleton(beanName);
    ...
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 1st level cache: beanName -> bean (fully created)
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 2nd level cache: beanName -> bean (early exposed, only instantiated)
            singletonObject = this.earlySingletonObjects.get(beanName);
            // 3rd level cache: beanName -> ObjectFactory
            if (singletonObject == null && allowEarlyReference) {
                // Obtain ObjectFactory from third‑level cache
                ObjectFactory
singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // Get bean from ObjectFactory (will invoke getEarlyBeanReference)
                    singletonObject = singletonFactory.getObject();
                    // Move bean to second‑level cache
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    // Remove from third‑level cache
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

The three caches are:

singletonObjects (first‑level): key = beanName, value = fully created bean after instantiation, property population, initialization, and all post‑processing.

earlySingletonObjects (second‑level): key = beanName, value = bean that has only been instantiated (exposed early) and may later be wrapped by a BeanPostProcessor.

singletonFactories (third‑level): key = beanName, value = ObjectFactory that can produce the early bean reference via getObject() . The factory stores the bean instance and applies SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference before returning it.

(2) Adding to the third‑level cache

When a bean is created, Spring registers a singleton factory:

this.addSingletonFactory(beanName, () -> {
    return this.getEarlyBeanReference(beanName, mbd, bean);
});

The call to getEarlyBeanReference adds an extra step: it invokes the BeanPostProcessor that creates the early AOP proxy (usually AbstractAutoProxyCreator ), allowing the proxy to be stored in the second‑level cache.

(3) Early‑exposed AOP proxy (earlyProxyReferences)

The method AbstractAutoProxyCreator.getEarlyBeanReference caches the original bean in earlyProxyReferences and then wraps it with an AOP proxy via wrapIfNecessary :

public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    this.earlyProxyReferences.put(cacheKey, bean);
    return wrapIfNecessary(bean, beanName, cacheKey);
}

Later, during postProcessAfterInitialization , Spring checks whether the bean has already been proxied. If the cached original bean matches the current bean, the proxy step is skipped; otherwise, the bean is wrapped.

(4) Interaction with BeanPostProcessors and AOP

When the bean finishes initialization, Spring calls applyBeanPostProcessorsAfterInitialization . If the bean has been early‑proxied, the proxy is already present in earlyProxyReferences , so the final AOP proxying step is avoided, and the early proxy is returned as the final bean instance.

(5) Property population and initialization of the early proxy

The early‑exposed bean (still only instantiated) is later fully populated and initialized. Because the AOP proxy holds a reference to the original target bean, the later property injection and initialization of the target automatically affect the proxy, ensuring that the final bean returned to the user is a fully initialized AOP proxy.

(6) Circular dependency with AOP – diagram

The article includes a diagram (omitted here) that visualises how bean A and bean B depend on each other, how bean A is early‑proxied, and how the three‑level cache resolves the circular reference while preserving AOP behavior.

Understanding these mechanisms means you have mastered one of the most complex parts of Spring's source code and reached an Alibaba‑level grasp of the framework.

AOPbackend developmentSpringCircular DependencyBeanPostProcessorThree-level Cache
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login 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.