How Spring Solves Circular Dependencies: Inside the Three‑Cache Mechanism

This article explains how Spring resolves circular dependencies for singleton beans using a three‑level cache, contrasts it with prototype limitations, provides a minimal container implementation that mimics the mechanism, and draws an analogy to the classic two‑sum algorithm to illustrate the core caching principle.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
How Spring Solves Circular Dependencies: Inside the Three‑Cache Mechanism

Three‑level cache for singleton circular dependencies

Spring resolves circular references for singleton beans by maintaining three Map instances in DefaultSingletonBeanRegistry:

singletonObjects – the final cache that holds fully initialized singleton instances.

singletonFactories – stores ObjectFactory callbacks capable of creating an early reference to a bean before its properties are populated.

earlySingletonObjects – holds the early (partially constructed) bean instances that have been exposed to other beans during the creation process.

The creation flow (simplified) is:

Object getBean(String name) {
    // 1. Check the fully initialized cache
    Object singleton = singletonObjects.get(name);
    if (singleton != null) return singleton;

    // 2. If the bean is currently being created, try to obtain an early reference
    if (isSingletonCurrentlyInCreation(name)) {
        Object earlyRef = earlySingletonObjects.get(name);
        if (earlyRef != null) return earlyRef;
        // 3. Use the factory to create an early reference
        ObjectFactory<?> factory = singletonFactories.get(name);
        if (factory != null) {
            earlyRef = factory.getObject();
            earlySingletonObjects.put(name, earlyRef);
            return earlyRef;
        }
    }

    // 4. No cache hit – create the bean instance
    Object bean = createBeanInstance(name);
    // 5. Register a factory that can expose an early reference
    singletonFactories.put(name, () -> getEarlyBeanReference(bean, name));
    // 6. Populate properties (which may trigger getBean for dependencies)
    populateBean(bean);
    // 7. Bean is fully initialized – move it to the final cache
    singletonObjects.put(name, bean);
    // 8. Clean temporary caches
    singletonFactories.remove(name);
    earlySingletonObjects.remove(name);
    return bean;
}

Step 5 creates the “stepping stone” that allows other beans to obtain a reference to bean before its full initialization, thereby breaking the circularity.

Prototype beans do not support circular references

When a bean is defined with scope="prototype", Spring cannot store a shared early reference. The framework detects a prototype‑in‑creation loop in AbstractBeanFactory and throws BeanCurrentlyInCreationException:

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

The check prevents infinite recursion that would otherwise lead to a StackOverflowError or OutOfMemoryError .

Minimal container that mimics Spring's circular‑dependency handling

The following self‑contained Java example demonstrates the same cache‑lookup principle. It creates singleton instances, stores them in a map, and injects fields recursively. The map plays the role of Spring's three‑level cache (the example collapses the three maps into one for brevity).

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

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

@SneakyThrows
private static <T> T getBean(Class<T> beanClass) {
    String beanName = beanClass.getSimpleName().toLowerCase();
    // 1. Return existing instance if present (early or final)
    if (cacheMap.containsKey(beanName)) {
        return (T) cacheMap.get(beanName);
    }
    // 2. Instantiate the bean
    Object instance = beanClass.getDeclaredConstructor().newInstance();
    // 3. Store the partially constructed instance (early reference)
    cacheMap.put(beanName, instance);
    // 4. Inject all fields recursively
    for (Field field : instance.getClass().getDeclaredFields()) {
        field.setAccessible(true);
        Class<?> depClass = field.getType();
        String depName = depClass.getSimpleName().toLowerCase();
        Object dep = cacheMap.containsKey(depName) ? cacheMap.get(depName) : getBean(depClass);
        field.set(instance, dep);
    }
    // 5. Return the fully populated bean
    return (T) instance;
}

After the loop finishes, cacheMap contains fully initialized objects for A and B, demonstrating that circular dependencies are resolved without special framework support.

Analogy with the “Two‑Sum” algorithm

The same cache‑lookup pattern appears in the classic two‑sum problem: while iterating over an array, store each number in a HashMap and look up the complement that would reach the target sum. If the complement is already present, the pair is found; otherwise, the current number is stored for future look‑ups.

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

In the bean‑creation scenario, the “key” is the bean name, the “value” is the bean instance, and the “lookup” occurs when a dependent bean requests its collaborator.

Key take‑aways

Spring’s three‑level cache ( singletonObjects, singletonFactories, earlySingletonObjects) enables property‑based injection of singleton beans that reference each other.

Prototype‑scoped beans lack a shared early reference, so circular dependencies are rejected with BeanCurrentlyInCreationException.

The core algorithm is a simple map‑lookup: create an object, store it, and reuse it for any subsequent dependency request.

Understanding this mechanism helps demystify the framework source and prepares you for interview questions about circular dependencies.

Spring three‑cache flow diagram
Spring three‑cache flow diagram
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-dependencyTwo SumThree Cache
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

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.