Backend Development 10 min read

How Does Spring Resolve Circular Dependencies? Inside the Three‑Level Cache

This article explains how Spring handles circular dependencies in singleton beans by using a three‑level cache, contrasts it with prototype beans that cannot participate, and demonstrates a minimal implementation that mimics Spring's approach, linking the concept to the classic two‑sum algorithm.

macrozheng
macrozheng
macrozheng
How Does Spring Resolve Circular Dependencies? Inside the Three‑Level Cache

Preface

Spring's solution to circular dependencies has become a popular Java interview question. The author reflects on the value of framework‑source questions and sets the stage for a deep dive into how Spring resolves such cycles.

Main Content

When discussing circular dependencies, Spring focuses on the default singleton bean scenario where beans reference each other via properties. Prototype‑scoped beans do not support circular references; attempting to create them triggers a

BeanCurrentlyInCreationException

in

AbstractBeanFactory

.

The failure occurs because creating a new instance of bean A requires injecting prototype bean B, which in turn requires creating prototype bean A, leading to a potential stack overflow or out‑of‑memory error. Spring therefore throws

BeanCurrentlyInCreationException

early.

Constructor‑based circular dependencies are not supported at all; the official documentation states that they cannot be resolved.

Spring Solves Circular Dependencies

Spring maintains three Map structures—often referred to as a three‑level cache—in

DefaultSingletonBeanRegistry

:

singletonObjects : the primary cache holding fully created singleton beans (the "singleton pool").

singletonFactories : stores factories that can create early references to beans.

earlySingletonObjects : holds early (incomplete) bean instances that are not yet fully initialized.

The latter two maps act as stepping stones: during bean creation, Spring places a factory in

singletonFactories

, creates an early reference in

earlySingletonObjects

, and finally moves the fully initialized bean into

singletonObjects

.

These steps are illustrated in the accompanying diagrams (images omitted for brevity).

The Essence of Circular Dependency

Beyond the Spring implementation, the core problem is simply managing a cache of already‑created objects so that dependent beans can be resolved without re‑instantiation. The author provides a minimal Java example that mimics Spring's behavior:

<code>private static Map<String, Object> cacheMap = new HashMap<>(2);

public static void main(String[] args) {
    Class[] classes = {A.class, B.class};
    for (Class aClass : classes) {
        getBean(aClass);
    }
    System.out.println(getBean(B.class).getA() == getBean(A.class));
    System.out.println(getBean(A.class).getB() == getBean(B.class));
}

private static <T> T getBean(Class<T> beanClass) throws Exception {
    String beanName = beanClass.getSimpleName().toLowerCase();
    if (cacheMap.containsKey(beanName)) {
        return (T) cacheMap.get(beanName);
    }
    Object object = beanClass.getDeclaredConstructor().newInstance();
    cacheMap.put(beanName, object);
    for (Field field : object.getClass().getDeclaredFields()) {
        field.setAccessible(true);
        Class<?> fieldClass = field.getType();
        String fieldBeanName = fieldClass.getSimpleName().toLowerCase();
        field.set(object, cacheMap.containsKey(fieldBeanName) ?
                cacheMap.get(fieldBeanName) : getBean(fieldClass));
    }
    return (T) object;
}
</code>

This code demonstrates that by checking the cache first, creating the bean, injecting its fields recursively, and storing the result, circular dependencies are resolved without special framework support.

What? The Problem Mirrors the Two‑Sum Algorithm

The author draws an analogy to the classic LeetCode "two sum" problem: just as a hashmap is used to find a complementary number in one pass, Spring's cache (the three‑level map) is used to locate a needed bean during creation.

Conclusion

If you find yourself stuck in the "source‑code swamp," understanding the underlying cache mechanism can clarify why Spring's solution appears complex. Recognizing the fundamental pattern helps you apply similar techniques in your own dependency‑injection implementations.

Javabackend developmentspringDependency InjectionCircular DependencyThree-level Cache
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

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.