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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
