Spring 6 Circular Dependencies and Early Proxies: The Real Limits of the Three-Level Cache
Baseline: Spring Framework 6 and Spring Boot 3. This article explains one specific case of proxy inconsistency; the conclusions should not be generalized to the behavior of every bean.
1. The Problem Model
A typical circular dependency:
A -> B -> Repository -> A
If dependency injection occurs before a bean is fully initialized, Spring may expose a reference early through the three-level cache. Subsequent initialization may also involve a BeanPostProcessor creating a proxy, resulting in:
Early Reference != Final Bean
2. What the Three-Level Cache Can Do
The ObjectFactory in the third-level cache can provide an early reference. A post-processor that implements SmartInstantiationAwareBeanPostProcessor can participate in early proxying through getEarlyBeanReference.
AbstractAutoProxyCreator records the early proxy reference to avoid creating the same automatic proxy again after initialization. However, not every BeanPostProcessor participates in early-reference processing.
An ordinary post-processor that wraps the bean only during the postProcessAfterInitialization phase can cause the identity of the early reference to differ from that of the final object. Whether this actually happens depends on the Spring version, post-processor ordering, and proxy type.
3. Recommendations for Spring Boot 3
Since Spring Boot 2.6, restrictions on circular dependencies have been stricter by default. Boot 3 projects should prioritize removing circular dependencies rather than using spring.main.allow-circular-references=true as the default fix.
Recommended order:
- Separate the responsibilities of the service and Repository;
- Use events or interfaces to reverse the direction of dependencies;
- Use @Lazy for dependencies that genuinely require lazy initialization, and assess the lifecycle implications;
- Check whether transaction, caching, async, exception-translation, and custom post-processors create proxies.
4. Troubleshooting
Record the actual Spring Framework version; inspect the bean's proxy type and Advisor; trace getEarlyBeanReference and postProcessAfterInitialization separately; check whether multiple post-processers wrap the bean redundantly; and review allowCircularReferences and BeanDefinition ordering.
The three-level cache does not guarantee that "all circular dependencies can be resolved." It is merely the mechanism by which the container can provide early references at specific points in the lifecycle. Ultimately, you still need a clear, maintainable dependency graph.