How Spring Solves Circular Dependencies: The 3‑Cache Trick Explained

This article explains how Spring resolves circular dependencies in singleton beans using a three‑level cache, why prototype beans cannot participate, provides a minimal example implementation, and draws an analogy to the classic two‑sum algorithm to illustrate the underlying principle.

Java Backend Technology
Java Backend Technology
Java Backend Technology
How Spring Solves Circular Dependencies: The 3‑Cache Trick Explained

Preface

Spring's way of handling circular dependencies has become a popular Java interview question in recent years. The article explores how Spring solves this problem for singleton beans, why prototype beans are excluded, and what the essential idea behind the solution is.

Prototype Limitation

Prototype‑scoped beans do not support circular dependencies. When a prototype bean is being created, Spring checks in AbstractBeanFactory and throws BeanCurrentlyInCreationException if a circular reference is detected.

if (isPrototypeCurrentlyInCreation(beanName)) {
    throw new BeanCurrentlyInCreationException(beanName);
}

The reason is straightforward: creating a new instance of A requires injecting a prototype B, which in turn requires creating a new instance of B that needs A, leading to infinite recursion or memory exhaustion.

Three‑Level Cache Mechanism

Spring maintains three maps in DefaultSingletonBeanRegistry, often referred to as the three‑level cache:

singletonObjects : the final singleton pool containing fully initialized beans.

singletonFactories : factories that can create early bean references.

earlySingletonObjects : holds early (incomplete) bean instances that are exposed to resolve circular references.

The latter two maps act as stepping stones: beans are first placed in earlySingletonObjects (or created via singletonFactories) so that other beans can reference them before they are fully initialized, and later moved to singletonObjects when construction completes.

Three‑level cache diagram
Three‑level cache diagram

Minimal Implementation

A simple Java example demonstrates how to mimic Spring's circular‑dependency handling using a cache map. The code creates bean instances, stores them in a map, and injects fields recursively, allowing A and B to reference each other without infinite recursion.

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

This code demonstrates that after injection, the cache contains fully formed beans, effectively solving the circular dependency.

Analogy to Two‑Sum

The author draws a parallel between the above cache‑lookup approach and the classic "two‑sum" algorithm: first check a map for the needed counterpart, and if not found, store the current element for future matches. Similarly, Spring first checks the cache for a required bean and, if absent, creates and stores it for later use.

class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int complement = target - nums[i];
            if (map.containsKey(complement)) {
                return new int[]{map.get(complement), i};
            }
            map.put(nums[i], i);
        }
        throw new IllegalArgumentException("No two sum solution");
    }
}

Both mechanisms rely on a lookup‑then‑store pattern to resolve dependencies efficiently.

Conclusion

If you have been stuck in the "source‑code swamp" trying to understand Spring's circular‑dependency handling, the three‑level cache concept and the minimal implementation above should clarify the core idea. Remember that Spring does far more than just dependency injection, so stepping back to see the bigger picture can be very helpful.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaspringdependency-injectioncircular-dependencyThree Cache
Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

0 followers
Reader feedback

How this landed with the community

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.