Why Spring Requires a Three‑Level Cache to Resolve Circular Dependencies Instead of a Two‑Level Cache
This article explains how Spring's bean lifecycle uses a three‑level cache (singletonObjects, earlySingletonObjects, singletonFactories) to break circular dependencies, especially when AOP proxies are involved, and why a two‑level cache alone cannot guarantee correct singleton behavior.
In everyday Spring development, circular dependencies between beans occur frequently, and Spring automatically resolves them for developers. This article analyzes how Spring solves bean circular dependencies and why it employs a three‑level cache rather than a two‑level cache.
Bean Lifecycle
Understanding the bean lifecycle in Spring is essential to grasp how circular dependencies are handled. The AbstractAutowireCapableBeanFactory.doCreateBean method starts bean creation by instantiating the bean (using reflection) without injecting @Autowired properties yet.
Next, populateBean fills bean properties; if a required dependency is not yet created, Spring recursively creates that dependency. After property injection, initializeBean invokes aware interfaces, BeanPostProcessor methods, and any custom init methods.
During initialization, Spring calls aware methods (e.g., setBeanName , setBeanClassLoader , setBeanFactory ) and executes postProcessBeforeInitialization and postProcessAfterInitialization for all BeanPostProcessor implementations. Notably, a bean that itself implements BeanPostProcessor will not have its own postProcessBeforeInitialization invoked until it is fully loaded into the singleton cache.
Three‑Level Cache for Circular Dependency Resolution
When populating properties, if Spring discovers a dependency that has not been created, it creates the dependent bean. Spring exposes the partially created bean via an ObjectFactory placed in the third‑level cache ( singletonFactories ), which holds a factory for an early bean reference.
The three caches are:
singletonObjects – first‑level cache for fully initialized singletons.
earlySingletonObjects – second‑level cache for early references (partially initialized beans).
singletonFactories – third‑level cache storing factories that can produce early references.
Scenario: AService depends on BService, and BService depends on AService.
AService is instantiated and its ObjectFactory is stored in the third‑level cache.
When injecting BService, Spring sees BService is missing and creates it, also storing its factory in the third‑level cache.
While injecting AService into BService, Spring retrieves the early reference from the third‑level cache, calls getObject() , which may return a proxy if AOP is applied.
The early reference is moved to the second‑level cache ( earlySingletonObjects ) to avoid creating multiple proxy instances.
If only two caches were used, each call to singletonFactory.getObject() could produce a new proxy for an AOP‑enhanced bean, breaking the singleton contract. The second‑level cache ensures the same proxy instance is reused, making the three‑level approach necessary when AOP is involved.
Summary
The bean loading process, combined with the three‑level cache, enables Spring to resolve circular dependencies safely, even when beans are wrapped by AOP proxies. Removing the second‑level cache would work only for non‑proxied beans, but would fail to maintain singleton integrity for proxied beans.
In practice, experimenting with AOP scenarios demonstrates why the three‑level cache is essential.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.