How Spring’s Third‑Level Cache Resolves Circular Dependencies

The article explains Spring’s three kinds of circular dependencies, the role of the first, second, and third‑level caches in the DefaultSingletonBeanRegistry, how the third‑level cache works with AOP proxies, why constructor injection cannot be solved, the @Lazy workaround, and the hidden bugs and best‑practice recommendations.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
How Spring’s Third‑Level Cache Resolves Circular Dependencies

1. Spring’s three types of circular dependencies

Spring circular dependency refers to a closed loop where singleton beans reference each other. There are three combinations:

Singleton + field/setter injection : ✅ solved perfectly by the third‑level cache.

Singleton + constructor injection : ❌ impossible to resolve, startup fails.

Prototype bean with any injection : ❌ cannot resolve, leads to infinite recursion and stack overflow.

Typical error stack trace:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'aService': Requested bean is currently in creation: Is there an unresolvable circular reference?

The underlying meaning is that the bean is still being instantiated and has not completed initialization, so the dependency loop cannot be closed.

2. Underlying source of the third‑level cache (DefaultSingletonBeanRegistry)

The three caches are members of Spring’s core registry, all implemented with Map structures and have distinct responsibilities:

// 1. First‑level cache: stores fully initialized, proxy‑completed beans ready for use
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 2. Second‑level cache: stores partially created beans (instantiated but properties not yet populated)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 3. Third‑level cache: stores ObjectFactory<?> that can create bean instances on demand
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

// Auxiliary set to mark beans that are currently in creation
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));

Explanation

First‑level cache: the finished bean warehouse, used by all business code.

Second‑level cache: a half‑finished warehouse used only for internal circular‑dependency injection, never exposed externally.

Third‑level cache: a processing pipeline that lazily creates half‑finished or proxy beans; it remains idle when not needed.

Key blind spot: the third‑level cache stores not the bean itself but a lambda ObjectFactory that can generate the bean.

3. Circular‑dependency flow with AOP proxy

Example with AService and BService that autowire each other and have AOP transaction enabled:

The container creates AService, checks all three caches (none found), and marks it as “currently creating”.

It reflects the constructor to instantiate the raw AService object; at this point bService is null and no proxy exists.

Core step: the ObjectFactory lambda for AService (which contains the AOP proxy creation logic) is stored in the third‑level cache.

During property population, the container discovers that AService needs BService and pauses AService creation to create BService.

It similarly instantiates the raw BService and stores its factory in the third‑level cache, marking it as “currently creating”.

When populating BService ’s properties, the container needs AService, finds the factory in the third‑level cache, and executes it.

The factory creates the AOP proxy for AService, stores the proxy in the second‑level cache, and removes the entry from the third‑level cache.

The proxy is injected into BService, completing BService ’s dependencies. BService finishes initialization, its proxy is moved to the first‑level cache, and its second/third‑level entries are cleared.

The container returns to AService, injects the fully initialized BService bean, and completes AService ’s initialization.

The AService proxy moves from the second‑level cache to the first‑level cache, and the “currently creating” mark is removed.

Core conclusion: the third‑level cache is only triggered when a circular dependency exists **and** a proxy (e.g., AOP) is required; otherwise it never runs and incurs zero performance cost.

4. Caveats

4.1 Can we drop the second‑level cache and keep only first + third?

Absolutely not. Doing so would cause duplicate proxies and transaction failures.

Scenario: without the second‑level cache, the same AService would be requested by both CService and BService, causing the third‑level factory to be invoked twice, producing two distinct proxy instances. This leads to inconsistent object equality, broken transaction propagation, and corrupted local caches.

The second‑level cache ensures that the first generated proxy is reused globally.

4.2 Why not store the half‑finished bean directly in the second‑level cache?

To achieve global performance optimization and follow the lazy‑loading principle:

About 90% of beans have no circular dependency and go straight to the first‑level cache.

If the third‑level cache were skipped and beans were placed directly into the second‑level cache, every bean would eagerly create a proxy, increasing startup time by over 30%.

The third‑level cache implements on‑demand proxy creation: only when a circular dependency is detected does it execute the proxy logic, resulting in zero overhead otherwise.

4.3 Why is constructor injection unsolvable for circular dependencies?

Reviewing the bean lifecycle:

Field injection: instantiate (empty object) → store in third‑level cache → populate properties, providing a time window for early exposure.

Constructor injection: the bean must receive all dependencies at instantiation time; there is no opportunity to store it in the third‑level cache before the dependent bean is needed, so the cycle cannot be broken.

Spring’s official stance: it does not attempt to resolve constructor‑based circular dependencies because they violate the Dependency Inversion principle.

5. @Lazy lazy injection mechanism

Many developers only use @Lazy without understanding its internals. Adding @Lazy causes Spring to inject a dynamic proxy placeholder instead of the real bean. The placeholder is only resolved when a method is actually invoked, thereby breaking the creation order and bypassing the circular‑dependency check. This is a workaround, not a true fix, and should be used only for legacy projects.

@Service
public class AService {
    @Autowired
    @Lazy // inject proxy placeholder, lazy‑load real bean
    private BService bService;
}

6. Hidden bugs caused by third‑level cache fallback

Symptoms

In production, an order service occasionally returns inconsistent results for the same order; local cache data becomes corrupted. The issue is rare and cannot be reproduced in test environments.

Root cause

OrderService and UserService have a bidirectional circular dependency. The third‑level cache successfully starts the application, but the second‑level cache holds an uninitialized proxy bean. If a request accesses the proxy before the bean’s internal cache is fully initialized, it reads an empty cache, leading to data inconsistency.

Solution

Disallow reliance on the third‑level cache fallback. Refactor shared user‑validation logic into an independent utility class to eliminate the circular dependency entirely.

Important conclusion: the third‑level cache is Spring’s compatibility fallback for legacy code, not a design best practice. Companies such as Alibaba and ByteDance enforce a zero‑tolerance policy for circular dependencies in new projects and forbid using the third‑level cache or @Lazy as a workaround.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AOPSpringConstructor InjectionCircular DependencyBean LifecycleLazy InjectionThird-Level Cache
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

0 followers
Reader feedback

How this landed with the community

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.