Backend Development 22 min read

Understanding and Resolving Circular Dependencies in Spring Framework

This article explains the concept of circular dependencies in Spring, reviews related fundamentals, demonstrates how Spring's three-level cache resolves singleton circular references, discusses limitations with prototype and constructor injection, and provides code examples and detailed source analysis for developers.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
Understanding and Resolving Circular Dependencies in Spring Framework

Introduction

Circular dependency is a common interview question. Before diving into the solution, we review two concepts: IoC (Inversion of Control) and the bean creation steps in Spring.

What is Circular Dependency

It occurs when bean A depends on bean B and bean B depends on bean A (or longer cycles). Example code:

public class BeanB {
    private BeanA beanA;
    public void setBeanA(BeanA beanA) {
        this.beanA = beanA;
    }
}

public class BeanA {
    private BeanB beanB;
    public void setBeanB(BeanB beanB) {
        this.beanB = beanB;
    }
}

Corresponding XML configuration (using setter injection):

<bean id="beanA" class="priv.starfish.BeanA">
  <property name="beanB" ref="beanB"/>
</bean>

<bean id="beanB" class="priv.starfish.BeanB">
  <property name="beanA" ref="beanA"/>
</bean>

If Spring instantiated A first, it would discover the dependency on B, instantiate B, then discover the dependency on A again, leading to an infinite loop.

Source Code Analysis

Spring resolves the problem using a three‑level cache during bean instantiation.

Bean Retrieval Process

The simplified flow is:

Start from getBean , which delegates to doGetBean .

transformedBeanName converts the requested name to the real bean name.

getSingleton(beanName) tries to fetch the bean from caches.

If a fully initialized instance exists, it is returned; otherwise a new bean is created.

The three caches are:

singletonObjects : fully initialized singleton instances (first‑level cache).

earlySingletonObjects : raw bean instances that have been instantiated but not yet populated (second‑level cache).

singletonFactories : factories that can produce early references, mainly for AOP proxy exposure (third‑level cache).

Key method getSingleton shows how the caches are consulted:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                ObjectFactory
singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

During bean creation, Spring registers an ObjectFactory in the third cache, then populates properties. When another bean needs the early reference, Spring can obtain it from the second or third cache.

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
    // ... instantiate bean ...
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }
    // populate properties, initialize bean, etc.
    // ... circular dependency check ...
    if (earlySingletonExposure) {
        Object earlySingletonReference = getSingleton(beanName, false);
        if (earlySingletonReference != null) {
            if (exposedObject == bean) {
                exposedObject = earlySingletonReference;
            }
        }
    }
    // ... add to singletonObjects ...
}

How Spring Solves Circular Dependency

1. Spring creates the raw instance of bean A and stores it in the third cache. 2. While populating A, it discovers the dependency on B and creates B, also storing its raw instance in the third cache. 3. When B needs A, Spring retrieves the early reference of A from the second cache (or creates it from the factory) and injects it, allowing B to finish initialization. 4. After B is fully initialized, Spring completes the property population and initialization of A, moving the final proxy (if any) into the first‑level cache.

Why a Three‑Level Cache Is Needed

If no AOP proxy is involved, the second‑level cache would be sufficient. However, Spring postpones AOP proxy creation until after property population. The third cache holds a factory that can create the proxy on demand, ensuring that beans participating in circular references receive the proxied instance without breaking the bean lifecycle.

Non‑Singleton Circular Dependency

Prototype‑scoped beans are not cached, so Spring cannot expose an early reference. Attempting a circular reference with prototype beans results in a BeanCurrentlyInCreationException :

Error creating bean with name 'beanA': Requested bean is currently in creation: Is there an unresolvable circular reference?

Constructor Injection Circular Dependency

When both beans use constructor injection, Spring cannot create an early reference because the bean is not instantiated until all constructor arguments are satisfied. This leads to an unsolvable circular dependency and the same exception as above. Switching to setter injection or avoiding constructor‑based cycles resolves the issue.

Summary for Interviews

Spring uses three caches: singletonObjects , earlySingletonObjects , and singletonFactories .

Singleton circular dependencies are solved by exposing a raw bean early (second‑level cache) and later completing its initialization.

Prototype beans and constructor‑based circular dependencies cannot be resolved and cause exceptions.

The third‑level cache enables AOP proxy creation without violating the bean lifecycle.

References: "Spring 源码深度解析" by 郝佳, various online articles and official Spring documentation.

JavaAOPIoCSpringBeanCircular Dependency
Full-Stack Internet Architecture
Written by

Full-Stack Internet Architecture

Introducing full-stack Internet architecture technologies centered on Java

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.