Backend Development 19 min read

Understanding Spring BeanFactory Caching and Circular Dependency Resolution

This article explains how Spring's BeanFactory creates and caches beans, details the three‑level cache mechanism (singleton, early‑singleton, and singleton‑factory) used to resolve circular dependencies, and walks through the relevant source code and lifecycle methods.

Top Architect
Top Architect
Top Architect
Understanding Spring BeanFactory Caching and Circular Dependency Resolution

Spring's BeanFactory acts as an IoC container that creates bean instances, initializes them, and stores them in caches for later use.

The bean creation process involves instantiating the bean, populating its properties, and then placing it into a cache, typically a simple Map<String, Object> for singleton beans.

First‑Level Cache

Instantiate bean A.

During property population, bean A needs bean B, which is not yet created.

Instantiate bean B.

When populating B, it requires A, which is still incomplete, leading to a circular loop and stack overflow.

Why not store A in the map after instantiation?

Because A is not fully initialized; storing it prematurely could cause runtime errors due to missing properties.

Second‑Level Cache (earlySingletonObjects)

A second cache holds partially created bean references, allowing circular dependencies to be broken without completing full initialization.

Instantiate A and place it in the second‑level cache.

During A's property population, create B and also place B in the second‑level cache.

When B needs A, retrieve the early reference from the second‑level cache, allowing both beans to finish initialization.

After both are fully initialized, move them to the first‑level cache and remove them from the second‑level cache.

Why a third cache?

To support AOP proxy creation, which must occur after bean instantiation but before property population. The third cache stores ObjectFactory lambdas that can produce early bean references for proxy generation.

Third‑Level Cache (singletonFactories)

After instantiating A, store an ObjectFactory for A in the third‑level cache.

When B needs A, retrieve the early reference via the factory, place it in the second‑level cache, and remove the factory.

Complete B's initialization, then move both beans to the first‑level cache.

The following code snippets illustrate key parts of Spring's implementation:

public class A {
    B b;
}
public class B {
    A a;
}
@Component
public class A {
    @Autowired
    B b;
}
@Component
public class B {
    @Autowired
    A a;
}

Spring bootstraps the context by invoking SpringApplication#run , which eventually calls AbstractApplicationContext#refresh and AbstractApplicationContext#finishBeanFactoryInitialization . During DefaultListableBeanFactory#preInstantiateSingletons , each bean definition is processed, invoking AbstractBeanFactory#getBean , which checks the three caches in order to resolve circular references.

public Object getBean(String name) throws BeansException {
    return doGetBean(name, null, null, false);
}

The DefaultSingletonBeanRegistry#getSingleton method demonstrates the cache lookup logic, first checking the singletonObjects map, then earlySingletonObjects, and finally singletonFactories if early references are allowed.

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;
}

Through this three‑level caching strategy, Spring efficiently resolves circular dependencies while supporting AOP proxy creation and other bean post‑processing steps.

BackendJavaSpringcachingCircular DependencyBeanFactory
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.