Introduction
You deploy a Spring Boot application and immediately see: BeanCurrentlyInCreationException. Two beans need each other, and Spring won't let them exist in that tangle. Fixing circular dependencies in Spring, explained simply, comes down to recognizing that the problem rarely belongs to the framework—it belongs to your design.
Spring Boot forbids circular dependencies by default, and that is good. This article walks you through why they happen, how to spot them early, and the architectural patterns that eliminate them. We'll move beyond quick fixes into lasting solutions that make your codebase cleaner and easier to test.
Usually a sign of two classes doing one job
The first instinct when you encounter a circular dependency is to blame Spring. The real culprit is almost always architectural: two classes have become so tightly coupled that they can't live without each other. This almost always means they're sharing responsibilities that belong in a single place.
Consider a common e-commerce scenario:
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void createOrder(Order order) {
// creates order
paymentService.notifyPaymentInitiated(order);
}
}
@Service
public class PaymentService {
private final OrderService orderService;
public PaymentService(OrderService orderService) {
this.orderService = orderService;
}
public void processPayment(Payment payment) {
// processes payment
orderService.updateOrderStatus(payment);
}
}Spring refuses to instantiate these beans. Why? Because OrderService needs PaymentService, and PaymentService needs OrderService. Neither can be created first.
But look closer at what's happening: both services are coordinating the workflow. That coordination logic—create an order, process payment, update status—belongs in neither service alone. It belongs in an orchestrator or workflow handler. The two classes are fighting over responsibility.
When you hit a circular dependency, ask: what workflow are these classes actually implementing together? That workflow should have its own home.
Extract the shared logic to a third bean
The cleanest fix is to extract the shared logic into a new service or orchestrator that both depend on, but don't depend on each other.
@Service
public class OrderService {
public void createOrder(Order order) {
// just creates the order
}
}
@Service
public class PaymentService {
public void processPayment(Payment payment) {
// just processes the payment
}
}
@Service
public class OrderFulfillmentOrchestrator {
private final OrderService orderService;
private final PaymentService paymentService;
public OrderFulfillmentOrchestrator(
OrderService orderService,
PaymentService paymentService) {
this.orderService = orderService;
this.paymentService = paymentService;
}
public void fulfillOrder(Order order) {
orderService.createOrder(order);
paymentService.processPayment(order.getPayment());
}
}Now each service has one reason to change. OrderService handles order logic. PaymentService handles payment logic. OrderFulfillmentOrchestrator handles the workflow. The dependency graph is clean: the orchestrator depends on both services, but neither service depends on the other.
This pattern scales well. As your orchestration logic grows, it becomes explicit and testable. You can mock both services in tests without fighting Spring's dependency injection. New team members see immediately where the workflow lives.
In larger applications, you might use a dedicated handler or use-case pattern:
@Service
public class CreateOrderUseCase {
private final OrderService orderService;
private final PaymentService paymentService;
public CreateOrderUseCase(
OrderService orderService,
PaymentService paymentService) {
this.orderService = orderService;
this.paymentService = paymentService;
}
public OrderResult execute(CreateOrderCommand cmd) {
Order order = orderService.createOrder(cmd.toOrder());
PaymentResult result = paymentService.processPayment(
order.getPayment()
);
return OrderResult.from(order, result);
}
}This approach makes the intent clear: "here's what happens when we create an order." It's the orchestration layer, and it should be visible and named accordingly.
Use events to decouple notifications
Sometimes the circular dependency stems from notification requirements. OrderService needs to tell PaymentService something happened, and PaymentService needs to tell OrderService the same. Events decouple this entirely.
Instead of direct method calls, services publish domain events that other services listen to:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
public OrderService(
OrderRepository orderRepository,
ApplicationEventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.eventPublisher = eventPublisher;
}
public void createOrder(Order order) {
Order saved = orderRepository.save(order);
eventPublisher.publishEvent(new OrderCreatedEvent(saved));
}
}
@Service
public class PaymentService {
private final PaymentRepository paymentRepository;
private final ApplicationEventPublisher eventPublisher;
public PaymentService(
PaymentRepository paymentRepository,
ApplicationEventPublisher eventPublisher) {
this.paymentRepository = paymentRepository;
this.eventPublisher = eventPublisher;
}
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
Payment payment = Payment.forOrder(event.getOrder());
Payment saved = paymentRepository.save(payment);
eventPublisher.publishEvent(new PaymentInitiatedEvent(saved));
}
}
public class OrderCreatedEvent {
private final Order order;
public OrderCreatedEvent(Order order) {
this.order = order;
}
public Order getOrder() { return order; }
}
public class PaymentInitiatedEvent {
private final Payment payment;
public PaymentInitiatedEvent(Payment payment) {
this.payment = payment;
}
public Payment getPayment() { return payment; }
}Now there's no dependency between OrderService and PaymentService. They communicate through events published to Spring's ApplicationEventPublisher. Each service can be tested in isolation. You can add new listeners (say, a notification service) without touching either service.
For distributed systems, you'd move to a message broker like Kafka or RabbitMQ, but the principle stays the same: decouple through events, not direct calls.
@Lazy is a workaround, not a fix
Spring offers @Lazy as an escape hatch for circular dependencies:
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(@Lazy PaymentService paymentService) {
this.paymentService = paymentService;
}
}With @Lazy, Spring doesn't inject the real bean. It injects a proxy that fetches the bean only when you call a method on it. This breaks the circular creation, but it doesn't solve the underlying problem.
We discourage @Lazy for several reasons:
- Hides the real issue. You've silenced Spring's warning, but the coupling is still there.
- Adds hidden latency. The first call to the lazy bean will trigger its creation, and that might happen at an inconvenient time in your request lifecycle.
- Harder to test. You need to remember to initialize the lazy proxy in unit tests, or you'll get NullPointerExceptions.
- Clutters intent. Readers see
@Lazyand wonder why—it signals "something's wrong here" without explaining what.
Use @Lazy only if you have a legitimate reason to defer creation, not to sidestep circular dependencies. Even then, document why.
Common mistakes
- Applying @Lazy everywhere. If you're using
@Lazyon multiple beans, you've got an architectural problem, not a dependency issue. Refactor instead. - Passing large orchestrators around. When you extract the workflow, don't make the orchestrator depend on ten other services. That's a sign you've extracted too little logic. Keep orchestrators focused.
- Events that loop back. If service A publishes an event that service B listens to, and service B publishes an event that service A listens to, you've recreated the same problem with events. Be careful about event chains.
- Injecting ApplicationContext to grab beans manually. This bypasses Spring's dependency injection entirely and makes tests very hard to write. Resist this urge.
- Ignoring the circular dependency warning in development. If you see it locally and silence it, it will bite you in production. Take it seriously now.
Key takeaways
- Circular dependencies are a design smell, not a Spring limitation. Spring's refusal to auto-wire them is a feature, not a bug.
- Most circular dependencies arise because two classes share a single responsibility. Extract that responsibility into a third bean or orchestrator.
- For notification patterns, use Spring's
ApplicationEventPublisherto decouple services completely. They won't need to know about each other. @Lazyis a workaround that masks the problem. Use it only for legitimate deferred-creation scenarios, never to escape circular dependency errors.- Well-designed services have clear responsibilities and minimal coupling. Circular dependencies are your code telling you to refactor.






Comments
Loading comments…