Foundation · Understand
Diagnose Circular Dependencies
Find constructor cycles, understand why early references are fragile, and redesign orchestration instead of hiding the dependency loop.
Primary source: Spring Framework 6.2 Reference: Circular Dependencies
Lesson 13 of 14
1d streakThe Architectural Question
Since Spring Boot 2.6 the container refuses circular references by default and fails fast, because a cycle almost always marks a responsibility split that never finished.
What this guide proves
- Detect a real constructor dependency cycle
- Identify the responsibility that should own orchestration
- Use events, ports, or a coordinator instead of lazy self-references
The 3-level singleton cache in DefaultSingletonBeanRegistry
To resolve circular dependencies among singleton beans, Spring's DefaultSingletonBeanRegistry implements a sophisticated 3-Level Cache architecture:
| Cache Tier | Internal Field Name | Stored Value Type | Purpose in Resolution Flow |
|---|---|---|---|
| Level 1 (Singleton Cache) | Map<String, Object> singletonObjects | Fully initialized bean instances | Serves ready beans to the application. Fast-path lookup. |
| Level 2 (Early Singleton Cache) | Map<String, Object> earlySingletonObjects | Raw unpopulated bean instances | Resolves circular references when multiple circular beans need the early instance. |
| Level 3 (Singleton Factories) | Map<String, ObjectFactory<?>> singletonFactories | ObjectFactory producing early references | Exposes an early reference or generates an early AOP proxy before initialization. |
- 011. Bean A is instantiated. Before populating its properties, Spring adds a singletonFactory into the Level 3 cache.
- 022. Bean A tries to inject Bean B. Bean B is not in any cache, so Spring starts creating Bean B.
- 033. Bean B tries to inject Bean A. It checks Level 1 (miss), Level 2 (miss), and finds Bean A's factory in Level 3.
- 044. Level 3 factory returns Bean A's early reference (or early AOP proxy), promoting it to Level 2 earlySingletonObjects.
- 055. Bean B completes injection and initialization, entering Level 1 singletonObjects.
- 066. Bean A completes injection of Bean B and finishes initialization, moving to Level 1.
Why constructor circular dependencies cannot be resolved
Notice that the 3-level cache relies entirely on exposing the raw object reference after the constructor finishes, before setter injection runs. With Constructor Injection, this is mathematically impossible:
Architectural Boundary · Do Not Confuse
Setter Injection Cycle (Resolvable via 3-Level Cache)
Constructor Injection Cycle (Deadlock at Startup)
In constructor injection, ServiceA cannot finish executing its constructor until ServiceB is supplied. But ServiceB cannot finish executing its constructor until ServiceA is supplied. Neither object can be instantiated, so neither can expose an early reference into the Level 3 cache. Startup deadlocks and Spring aborts immediately.
Spring Boot 2.6+ Disables Circular Dependencies by Default
Architectural patterns to break dependency cycles
When a circular dependency arises, do not hack it with @Lazy. Use one of three architectural patterns:
- 01Pattern 1 (Extract Coordinator): If OrderService and NotificationService call each other, create OrderCheckoutCoordinator that calls OrderService, then calls NotificationService.
- 02Pattern 2 (Domain Events): Instead of OrderService injecting NotificationService, have OrderService publish an OrderCreatedEvent via ApplicationEventPublisher. NotificationService listens via @EventListener.
- 03Pattern 3 (Interface Segregation): Extract a smaller read-only interface or shared repository that both services can safely share.
Break it deliberately: The BeanCurrentlyInCreationException
When two services declare mutual constructor dependencies, Spring fails fast with a clear diagnostic trace:
@Service
public class OrderService {
public OrderService(PaymentService paymentService) {}
}
@Service
public class PaymentService {
public PaymentService(OrderService orderService) {}
}APPLICATION FAILED TO START *************************** Description: The dependencies of some of the beans in the application context form a cycle: ┌─────┐ | orderService defined in file [OrderService.class] ↑ ↓ | paymentService defined in file [PaymentService.class] └─────┘ Action: Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans.
Check your understanding
Active Recall Checkpoint
Why is Spring's 3-level singleton cache unable to resolve circular dependencies that use constructor injection?
Hint: When is a bean added to the Level 3 cache?
Show answer & explanation
The Level 3 cache stores an ObjectFactory for a bean after its constructor has executed. With constructor injection, the constructor cannot finish executing until its dependency is provided, so the bean can never be placed into the Level 3 cache.
Active Recall Checkpoint
What architectural decoupling pattern replaces a circular call between OrderService and NotificationService with an event-driven flow?
Hint: Think about Spring's event publishing subsystem.
Show answer & explanation
Publish an application event (e.g. OrderPlacedEvent) via ApplicationEventPublisher from OrderService, and handle it asynchronously or reactively using an @EventListener method in NotificationService.
Runtime Architecture & Flow
DefaultSingletonBeanRegistry
Spring 3-Level Singleton Cache Simulator
1. ServiceA Instantiation
ServiceA constructor executes. Before populating properties, Spring registers an ObjectFactory for ServiceA in Level 3 (singletonFactories).
singletonFactories
Holds factories producing early references or AOP proxies
earlySingletonObjects
Holds early unpopulated instances to resolve cycles
singletonObjects
Fully initialized, wired singletons serving application traffic
> DefaultListableBeanFactory: Eagerly cached [serviceA] factory in singletonFactories (L3)
Working Implementation
Code Walkthrough
Create two services that depend on each other, observe startup failure, then extract a coordinator service that breaks the cycle.
package com.example.core;
import org.springframework.stereotype.Service;
public class CoordinatorDecouplingLab {
// FLAGGED: Mutual constructor injection crashes startup with BeanCurrentlyInCreationException
// OrderService(PaymentService) <---> PaymentService(OrderService)
// PRODUCTION REFACTOR: Unidirectional orchestration via Coordinator
@Service
public static class OrderService {
public String createOrder(String itemId) {
return "ORD-" + itemId.hashCode();
}
}
@Service
public static class PaymentService {
public boolean processPayment(String orderId, double amount) {
return amount > 0;
}
}
@Service
public static class CheckoutCoordinator {
private final OrderService orderService;
private final PaymentService paymentService;
public CheckoutCoordinator(OrderService orderService, PaymentService paymentService) {
this.orderService = orderService;
this.paymentService = paymentService;
}
public boolean executeCheckout(String itemId, double amount) {
String orderId = orderService.createOrder(itemId);
return paymentService.processPayment(orderId, amount);
}
}
}Synthesis & Production Verification
Production Acceptance Criteria
- Reproduce the startup failure from a pure constructor cycle
- Break the cycle with a coordinator service both sides depend on
- Explain why @Lazy masks rather than repairs the design problem
Key Takeaway
Spring's 3-level singleton cache resolves setter circularities via early references, but constructor circular dependencies deadlock and fail fast at startup by design.
Architectural & Technical Review
Core Interview Questions
Q01What are the three tiers of Spring's 3-level singleton cache in DefaultSingletonBeanRegistry?Show answer
The tiers are: Level 1 (singletonObjects: fully initialized singletons), Level 2 (earlySingletonObjects: raw unpopulated instances), and Level 3 (singletonFactories: ObjectFactories producing early references or early AOP proxies).
Q02Why can the 3-level cache resolve setter circular dependencies but mathematically cannot resolve constructor circular dependencies?Show answer
The Level 3 cache requires the bean constructor to finish so the raw reference can be stored. In constructor cycles, neither constructor can complete without the other, deadlocking startup before either can be cached.
Q03What architectural patterns break circular dependencies without resorting to the @Lazy hack?Show answer
Extract an orchestrator/coordinator service that depends on both services, publish domain events via ApplicationEventPublisher, or extract shared read-only ports.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Using @Lazy as a quick band-aid for circular dependencies — @Lazy generates a runtime proxy that masks an architectural flaw, leaving tight bidirectional coupling in place.
- Pitfall: Re-enabling circular references in Spring Boot configuration — Setting spring.main.allow-circular-references=true re-enables fragile setter cycles that complicate initialization and proxy generation.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources