How Spring Solves Circular Dependencies: Deep Dive into Three-Level Caches
This article explains why Spring’s circular dependency problem only occurs with singleton beans, how constructor versus setter injection affects resolution, and why Spring uses a three‑level cache (singletonObjects, earlySingletonObjects, singletonFactories) to expose incomplete beans and support proxy creation without breaking the bean lifecycle.
What Is a Circular Dependency?
Two beans that reference each other, e.g., class A autowires B and class B autowires A, create a circular dependency that can cause bean creation to stall.
<code>@Service
public class A {
@Autowired
private B b;
}
@Service
public class B {
@Autowired
private A a;
}
// or a bean that depends on itself
@Service
public class A {
@Autowired
private A a;
}</code>Spring resolves such cycles only for singleton beans and when not all injections are constructor‑based.
Why Must Beans Be Singletons?
Prototype‑scoped beans would cause infinite instantiation (A1 → B1 → A2 → B2 …), so Spring limits circular‑dependency resolution to singletons.
Why Constructor Injection Alone Fails
Bean creation follows three steps: 1) instantiate, 2) populate properties (setter injection), 3) initialize. If both A and B use only constructor injection, the container cannot obtain an incomplete instance from the early cache, so the cycle fails.
Mixed Constructor and Setter Injection
When one bean uses setter injection and the other uses constructor injection, the cycle can succeed because the partially created bean is stored in the early cache and later injected.
Full Process Spring Uses to Break Circular Dependencies
Spring maintains three caches for singleton beans:
SingletonObjects (first‑level) – fully created beans.
EarlySingletonObjects (second‑level) – beans that are instantiated but not yet populated.
SingletonFactories (third‑level) – factories that can create an early reference, allowing proxy creation.
When a bean is being created, Spring registers a factory in the third‑level cache:
<code>addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));</code>During property injection, if a dependent bean is requested, the container checks the caches in order. If the bean is still in creation, the third‑level factory provides an early reference (potentially a proxy) which is placed into the second‑level cache, allowing the dependent bean to complete.
Why a Third‑Level Cache Is Needed
The third‑level cache enables Spring to expose a bean reference before full initialization, which is essential for creating AOP proxies without violating the bean lifecycle. Without this step, proxies would be created too early.
Conclusion
Spring solves circular dependencies by exposing incomplete singleton beans through a three‑level cache, allowing setter injection to succeed and enabling proxy creation at the correct lifecycle stage. Constructor‑only injection still fails, and the bean name’s alphabetical order can affect resolution.
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.
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.