Circular dependency injection errors can bring down your entire Spring Boot application at startup. If you've ever seen a BeanCurrentlyInCreationException or a dense stack trace pointing to beans that need each other to initialize, you know how frustrating this is. Spring Boot 2.6 made this worse—or better, depending on how you look at it—by failing fast on circular references instead of silently working around them. This shift forces you to fix the real architectural problem rather than patch over it.
The good news: circular dependencies are almost always solvable. You just need to understand why they happen and which refactoring technique fits your situation. This guide walks through the detection, root causes, and practical solutions using patterns that work in real production applications.
Understanding Circular Dependencies in Spring
A circular dependency occurs when Bean A requires Bean B, and Bean B requires Bean A. Spring's dependency injection container can't satisfy both at the same time—it's a deadlock at startup. For example:
@Component
public class UserService {
private final OrderService orderService;
public UserService(OrderService orderService) {
this.orderService = orderService;
}
}
@Component
public class OrderService {
private final UserService userService;
public OrderService(UserService userService) {
this.userService = userService;
}
}
When Spring tries to create UserService, it needs OrderService. But creating OrderService requires UserService, which hasn't finished constructing yet. Neither bean can complete initialization—Spring throws an error and the application fails to start.
Why Spring 2.6 Changed the Default Behavior
Prior to Spring Boot 2.6, the framework's default behavior was spring.main.allow-circular-references=true. This meant Spring would try to work around circular dependencies, sometimes leaving beans in a partially initialized state. It worked, but it masked poor architectural decisions and could lead to subtle bugs at runtime.
Spring Boot 2.6+ flipped the default to false. Now Spring fails immediately and loudly when it detects a circular reference. This is actually a feature—it forces you to fix the design rather than work around it. The error message is often the best feedback you'll get about your application structure.
If you're upgrading from Spring Boot 2.5 to 2.6 or later and suddenly see circular dependency errors, don't disable the check—that just hides the problem. Your application has a design issue worth fixing.
How to Detect Circular Dependencies
The error stack trace is your first clue. A BeanCurrentlyInCreationException will show you the chain of bean creations that lead to the cycle. Read it carefully—it often lists the exact beans involved and the order they tried to initialize in.
If the trace isn't clear, or if you want to visualize dependencies before startup fails, run:
gradle build -x test --info 2>&1 | grep -i "circular\|creating bean\|considering"
Or enable debug logging in your application.properties:
logging.level.org.springframework.beans=DEBUG
Spring will log each bean it's about to create, which helps you trace the chain manually. You can also use IDE plugins or online tools that parse your Spring configuration and generate a dependency graph.
Solution 1: Refactor to Break the Cycle with Constructor Injection
The cleanest fix is architectural: eliminate the actual circular need. Analyze what each bean does and whether the dependency is really necessary.
Often, you'll find that the dependency is bidirectional but only needed in one direction. For example, UserService might call OrderService.findByUser(), but OrderService doesn't actually need UserService—that import was added speculatively or left over from a refactor.
// After refactoring: remove the circular dependency
@Component
public class UserService {
private final OrderService orderService;
public UserService(OrderService orderService) {
this.orderService = orderService;
}
}
@Component
public class OrderService {
// No dependency on UserService anymore
public List<Order> findByUser(String userId) {
// ...
}
}
When to use this: This is the preferred solution. It improves code clarity and makes testing easier.
Solution 2: Use Setter or Field Injection
If both beans genuinely need each other (rare, but it happens in complex systems), you can defer initialization of one dependency:
@Component
public class UserService {
private final OrderService orderService;
private OrderService lazyOrderService;
public UserService(OrderService orderService) {
this.orderService = orderService;
}
@Autowired
public void setOrderService(OrderService orderService) {
this.lazyOrderService = orderService;
}
}
This works because setter injection happens after both beans are partially constructed. By the time setOrderService() is called, the UserService bean already exists, so OrderService can finish initializing with a reference to it.
Trade-off: Setter injection is less explicit than constructor injection—calling code must verify dependencies are actually wired. Use sparingly.
Solution 3: Use ObjectProvider or ObjectFactory
For optional or late-binding dependencies, Spring provides ObjectProvider<T> and ObjectFactory<T>:
@Component
public class UserService {
private final ObjectProvider<OrderService> orderServiceProvider;
public UserService(ObjectProvider<OrderService> orderServiceProvider) {
this.orderServiceProvider = orderServiceProvider;
}
public void processUser(String userId) {
OrderService orderService = orderServiceProvider.getIfAvailable();
if (orderService != null) {
orderService.findByUser(userId);
}
}
}
The container resolves ObjectProvider without requiring the underlying OrderService to exist at construction time. You request it lazily, only when you need it.
ObjectProvider is also useful for optional dependencies—if a bean might not be defined, getIfAvailable() returns an empty Optional instead of throwing an error.
Solution 4: Use @Lazy or PostConstruct
You can mark a dependency as @Lazy to defer its initialization until first use:
@Component
public class UserService {
private final OrderService orderService;
public UserService(@Lazy OrderService orderService) {
this.orderService = orderService;
}
}
Or initialize the dependency after the bean is constructed:
@Component
public class UserService {
@Autowired
private OrderService orderService;
@PostConstruct
public void init() {
// orderService is now available
}
}
These approaches defer dependency resolution, giving Spring time to create both beans before wiring them together.
Monitoring Circular Dependency Failures in Production
Even after you've fixed circular dependencies in development, startup issues can slip through in production, especially in larger teams or monolithic apps. Using error tracking for Spring Boot helps you catch initialization failures and other startup errors as soon as they occur.
Services like LightTrace capture the full stack trace, affected versions, and release information when your application fails to start. Combined with alert rules, you'll know the moment a deployment introduces a bean initialization problem, long before it impacts users.
Startup failures don't generate traditional error events—your app crashes before users see it. Error tracking is essential for catching these issues in staging or production.
Prevention: Code Review and Testing
The best cure is prevention. In code review, watch for:
- Service-to-service imports that flow in both directions
- Bidirectional relationships that could be unidirectional
- Multiple constructor parameters that hint at feature creep
In tests, instantiate your Spring context with @SpringBootTest and watch for BeanCurrentlyInCreationException or initialization failures during the test setup phase. If a test can't even start the context, your application won't start in production either.
Start tracking errors in minutes
Circular dependencies are a symptom of architectural debt, but they're fixable. Once you've refactored your beans, you need visibility into what breaks in production. Start free with LightTrace to track startup errors, release health, and crashes in your Spring Boot applications—no credit card required.