Backend Development 9 min read

How Spring Solves Circular Dependencies and the Underlying Essence

This article explains Spring's three‑level cache mechanism for breaking circular dependencies in singleton beans, demonstrates a simplified implementation with Java code, draws an analogy to the classic Two‑Sum algorithm, and highlights the core idea behind dependency injection.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
How Spring Solves Circular Dependencies and the Underlying Essence

Spring's handling of circular dependencies is a common Java interview topic, especially for singleton beans where two or more beans reference each other.

The framework uses three internal maps— singletonObjects , singletonFactories , and earlySingletonObjects —often referred to as a three‑level cache. The first stores fully created singletons, while the latter two act as temporary placeholders during bean creation.

When a bean is being created, Spring first checks if it already exists in the cache; if not, it creates the instance, puts it into the cache, and then injects its fields. If a field's dependency is already in the cache, it is injected directly; otherwise, Spring recursively creates the required bean.

Below is a simplified Java example that mimics this behavior using a static Map<String, Object> as the cache:

private static Map
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 getBean(Class
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;
}

The core idea is that the cache holds early references (similar to the second and third maps) so that circular references can be resolved without infinite recursion.

The article also draws a parallel to the classic LeetCode "Two Sum" problem, showing that the same hashmap‑based lookup strategy used to find a pair of numbers adds up to a target is analogous to Spring's bean lookup during dependency injection.

In summary, understanding the three‑level cache and the hashmap lookup pattern helps demystify Spring's circular‑dependency resolution and provides a practical way to implement a lightweight version yourself.

BackendJavaSpringdependency injectionCircular DependencyTwo Sum
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow 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.