Your Spring Boot app refuses to start, and the error message shows a dependency cycle between your beans. This isn’t just a technical problem—it’s usually a sign your code’s architecture needs rethinking.
TLDR - Quick Fix
CircularDependencyException means two or more beans depend on each other in a circle. Spring can’t create them because each one needs the other to exist first.
// ❌ PROBLEM: UserService needs OrderService, OrderService needs UserService
@Service
public class UserService {
@Autowired
private OrderService orderService;
}
@Service
public class OrderService {
@Autowired
private UserService userService;
}
// ✅ QUICK FIX: Use @Lazy to break the cycle
@Service
public class UserService {
@Autowired
@Lazy
private OrderService orderService;
}
Quick checklist:
- Check the error message for which beans form the cycle
- Apply
@Lazyto one dependency to break the loop temporarily - Refactor your design—circular dependencies indicate poor separation of concerns
- Consider extracting shared logic into a third service
- Use events or interfaces to decouple services
Let’s explore why circular dependencies happen, how to fix them properly, and how to design your code to avoid them altogether.
What Is a Circular Dependency?
A circular dependency occurs when Bean A needs Bean B, and Bean B needs Bean A. Spring tries to create both but gets stuck—it can’t finish creating either one because each requires the other to be complete.
Here’s the lifecycle that fails:
1. Spring: "Let me create UserService"
2. Spring: "UserService needs OrderService... let me create that"
3. Spring: "OrderService needs UserService... but I'm still creating it!"
4. Spring: "I give up" → CircularDependencyException
The error looks like this:
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| userService defined in file [UserService.class]
↑ ↓
| orderService defined in file [OrderService.class]
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default.
Update your application to remove the dependency cycle between beans.
This is actually a good thing—Spring is preventing you from running code with problematic architecture. Let’s see how to fix it properly.
How Circular Dependencies Happen
Scenario 1: Business logic split incorrectly
You have two services that legitimately need to call each other’s methods:
@Service
public class UserService {
@Autowired
private OrderService orderService;
public void deleteUser(Long userId) {
// Need to cancel all orders first
orderService.cancelOrdersForUser(userId);
userRepository.deleteById(userId);
}
}
@Service
public class OrderService {
@Autowired
private UserService userService;
public void createOrder(Long userId, Order order) {
// Need to validate user exists
User user = userService.getUser(userId);
order.setUser(user);
orderRepository.save(order);
}
}
Both services genuinely need functionality from the other. This looks reasonable at first but creates the circular dependency.
Scenario 2: Shared state or caching
@Service
public class CacheService {
@Autowired
private DataService dataService; // Needs to load data
public void refreshCache() {
dataService.loadAll();
}
}
@Service
public class DataService {
@Autowired
private CacheService cacheService; // Needs to invalidate cache
public void updateData(Data data) {
repository.save(data);
cacheService.invalidate();
}
}
Scenario 3: Event handling gone wrong
@Service
public class NotificationService {
@Autowired
private AuditService auditService;
public void sendNotification(String message) {
// Send notification
auditService.log("Notification sent: " + message);
}
}
@Service
public class AuditService {
@Autowired
private NotificationService notificationService;
public void log(String event) {
repository.save(new AuditLog(event));
// Notify admins of critical events
if (isCritical(event)) {
notificationService.sendNotification(event);
}
}
}
All these scenarios have better solutions than circular dependencies.
Solution #1: Use @Lazy (Quick Fix)
The fastest way to get your app running is adding @Lazy to one of the dependencies:
@Service
public class UserService {
@Autowired
@Lazy // Inject a proxy instead of the real bean
private OrderService orderService;
public void deleteUser(Long userId) {
orderService.cancelOrdersForUser(userId);
userRepository.deleteById(userId);
}
}
@Service
public class OrderService {
@Autowired
private UserService userService; // Normal injection
public void createOrder(Long userId, Order order) {
User user = userService.getUser(userId);
order.setUser(user);
orderRepository.save(order);
}
}
How it works: @Lazy tells Spring to inject a proxy object instead of the actual OrderService bean. The proxy is created immediately, but the real OrderService isn’t created until you first call a method on it. By that time, UserService is fully created, so the cycle is broken.
When to use this:
- You need a working app RIGHT NOW
- You’re in legacy code and can’t refactor yet
- The circular dependency is genuinely acceptable for your use case (rare)
Why this isn’t ideal:
- It hides the architectural problem
- Makes code harder to test (proxies behave slightly differently)
- Can mask performance issues (lazy loading overhead)
- Doesn’t solve the underlying design flaw
Think of @Lazy as a temporary band-aid, not a long-term solution.
Solution #2: Extract Shared Logic (Best Practice)
Most circular dependencies mean you have shared logic that should live in its own service. Let’s refactor the first example:
Before (circular):
@Service
public class UserService {
@Autowired
private OrderService orderService;
public void deleteUser(Long userId) {
orderService.cancelOrdersForUser(userId);
userRepository.deleteById(userId);
}
public User getUser(Long userId) {
return userRepository.findById(userId).orElseThrow();
}
}
@Service
public class OrderService {
@Autowired
private UserService userService;
public void createOrder(Long userId, Order order) {
User user = userService.getUser(userId);
order.setUser(user);
orderRepository.save(order);
}
public void cancelOrdersForUser(Long userId) {
List<Order> orders = orderRepository.findByUserId(userId);
orders.forEach(o -> o.setStatus(OrderStatus.CANCELLED));
orderRepository.saveAll(orders);
}
}
After (no cycle):
// New service for orchestration logic
@Service
public class UserOrderService {
@Autowired
private UserService userService;
@Autowired
private OrderService orderService;
public void deleteUserWithOrders(Long userId) {
orderService.cancelOrdersForUser(userId);
userService.deleteUser(userId);
}
public Order createOrderForUser(Long userId, Order order) {
User user = userService.getUser(userId);
return orderService.createOrder(user, order);
}
}
// UserService - focused on users only
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUser(Long userId) {
return userRepository.findById(userId).orElseThrow();
}
public void deleteUser(Long userId) {
userRepository.deleteById(userId);
}
}
// OrderService - focused on orders only
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
public Order createOrder(User user, Order order) {
order.setUser(user);
return orderRepository.save(order);
}
public void cancelOrdersForUser(Long userId) {
List<Order> orders = orderRepository.findByUserId(userId);
orders.forEach(o -> o.setStatus(OrderStatus.CANCELLED));
orderRepository.saveAll(orders);
}
}
Now there’s no cycle:
UserServiceonly manages users (no dependency onOrderService)OrderServiceonly manages orders (no dependency onUserService)UserOrderServiceorchestrates operations that need both- Controllers call
UserOrderServicefor complex operations
This follows the Single Responsibility Principle—each service has one reason to change.
Solution #3: Use Spring Events (Decoupling)
Events let you decouple services so they don’t directly depend on each other. Perfect for the notification/audit scenario:
Before (circular):
@Service
public class NotificationService {
@Autowired
private AuditService auditService;
public void sendNotification(String message) {
// Send notification
auditService.log("Notification sent: " + message);
}
}
@Service
public class AuditService {
@Autowired
private NotificationService notificationService;
public void log(String event) {
repository.save(new AuditLog(event));
if (isCritical(event)) {
notificationService.sendNotification(event);
}
}
}
After (event-based, no cycle):
// Define events
public class NotificationSentEvent {
private final String message;
public NotificationSentEvent(String message) {
this.message = message;
}
public String getMessage() { return message; }
}
public class CriticalAuditEvent {
private final String event;
public CriticalAuditEvent(String event) {
this.event = event;
}
public String getEvent() { return event; }
}
// NotificationService publishes events, doesn't call AuditService
@Service
public class NotificationService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void sendNotification(String message) {
// Send notification
eventPublisher.publishEvent(new NotificationSentEvent(message));
}
}
// AuditService publishes events, doesn't call NotificationService
@Service
public class AuditService {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private AuditRepository repository;
public void log(String event) {
repository.save(new AuditLog(event));
if (isCritical(event)) {
eventPublisher.publishEvent(new CriticalAuditEvent(event));
}
}
// Listen for notification events
@EventListener
public void onNotificationSent(NotificationSentEvent event) {
log("Notification sent: " + event.getMessage());
}
}
// Separate listener for critical events (optional)
@Component
public class CriticalEventHandler {
@Autowired
private NotificationService notificationService;
@EventListener
public void onCriticalAudit(CriticalAuditEvent event) {
notificationService.sendNotification("CRITICAL: " + event.getEvent());
}
}
Benefits:
- Services don’t know about each other
- Easy to add new listeners without modifying existing code
- Natural fit for cross-cutting concerns (logging, monitoring, notifications)
- Supports async processing with
@Async
Solution #4: Use Constructor Injection Strategically
Mixing constructor and setter injection can sometimes break cycles, though this isn’t ideal architecture:
@Service
public class UserService {
private final UserRepository userRepository;
private OrderService orderService; // Setter injection
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
public void setOrderService(OrderService orderService) {
this.orderService = orderService;
}
public void deleteUser(Long userId) {
orderService.cancelOrdersForUser(userId);
userRepository.deleteById(userId);
}
}
@Service
public class OrderService {
private final UserService userService; // Constructor injection
public OrderService(UserService userService) {
this.userService = userService;
}
public void createOrder(Long userId, Order order) {
User user = userService.getUser(userId);
order.setUser(user);
orderRepository.save(order);
}
}
This works because:
- Spring creates
UserServicewith just theUserRepository - Then creates
OrderServicewith the now-completeUserService - Finally calls
setOrderService()to complete the wiring
Downsides:
- You lose the immutability benefits of constructor injection
- Still doesn’t fix the architectural issue
- Mixes injection styles inconsistently
I’d only use this when you absolutely can’t refactor immediately.
Solution #5: Use Interfaces to Break Hard Dependencies
Sometimes circular dependencies exist because of tight coupling. Interfaces can help:
Before:
@Service
public class PaymentService {
@Autowired
private OrderService orderService;
public void processPayment(Long orderId) {
Order order = orderService.getOrder(orderId);
// Process payment
orderService.markAsPaid(orderId);
}
}
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
public void createOrder(Order order) {
orderRepository.save(order);
paymentService.processPayment(order.getId());
}
}
After (using callback interface):
// Define callback interface
public interface PaymentCallback {
void onPaymentComplete(Long orderId);
}
@Service
public class PaymentService {
// No direct dependency on OrderService!
public void processPayment(Long orderId, PaymentCallback callback) {
// Process payment
callback.onPaymentComplete(orderId);
}
}
@Service
public class OrderService implements PaymentCallback {
@Autowired
private PaymentService paymentService;
public void createOrder(Order order) {
Order savedOrder = orderRepository.save(order);
// Pass 'this' as the callback
paymentService.processPayment(savedOrder.getId(), this);
}
@Override
public void onPaymentComplete(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
}
}
PaymentService no longer depends on OrderService—it just calls a callback. The cycle is broken.
Real-World Example: E-commerce Platform
Let’s see how these solutions work together in a realistic scenario. Here’s a poorly designed system with circular dependencies:
// ❌ BAD DESIGN - Multiple circular dependencies
@Service
public class ProductService {
@Autowired private OrderService orderService;
@Autowired private InventoryService inventoryService;
@Autowired private PricingService pricingService;
public Product getProductWithPrice(Long id) {
Product product = repository.findById(id).orElseThrow();
product.setCurrentPrice(pricingService.calculatePrice(product));
return product;
}
}
@Service
public class OrderService {
@Autowired private ProductService productService;
@Autowired private InventoryService inventoryService;
@Autowired private ShippingService shippingService;
public Order createOrder(List<Long> productIds) {
// Needs products, inventory, shipping...
}
}
@Service
public class InventoryService {
@Autowired private ProductService productService;
@Autowired private OrderService orderService;
public void reserveStock(Long productId) {
// Circular dependencies everywhere!
}
}
// And so on... this is a mess
Here’s the refactored version using the patterns we’ve learned:
// ✅ GOOD DESIGN - Clear layers, no cycles
// === Domain Services (no dependencies on each other) ===
@Service
public class ProductService {
@Autowired private ProductRepository repository;
public Product findById(Long id) {
return repository.findById(id).orElseThrow();
}
public List<Product> findByIds(List<Long> ids) {
return repository.findAllById(ids);
}
}
@Service
public class InventoryService {
@Autowired private InventoryRepository repository;
public void reserveStock(Long productId, int quantity) {
Inventory inventory = repository.findByProductId(productId)
.orElseThrow();
inventory.reserve(quantity);
repository.save(inventory);
}
public boolean isAvailable(Long productId, int quantity) {
return repository.findByProductId(productId)
.map(inv -> inv.getAvailable() >= quantity)
.orElse(false);
}
}
@Service
public class PricingService {
public BigDecimal calculatePrice(Product product, User user) {
// Pricing logic only
BigDecimal basePrice = product.getBasePrice();
// Apply discounts, etc.
return basePrice;
}
}
// === Application Services (orchestrate domain services) ===
@Service
public class OrderProcessingService {
@Autowired private ProductService productService;
@Autowired private InventoryService inventoryService;
@Autowired private PricingService pricingService;
@Autowired private OrderRepository orderRepository;
@Autowired private ApplicationEventPublisher eventPublisher;
@Transactional
public Order createOrder(Long userId, List<OrderItem> items) {
// 1. Validate products exist
List<Long> productIds = items.stream()
.map(OrderItem::getProductId)
.collect(Collectors.toList());
List<Product> products = productService.findByIds(productIds);
// 2. Check inventory
for (OrderItem item : items) {
if (!inventoryService.isAvailable(item.getProductId(), item.getQuantity())) {
throw new OutOfStockException(item.getProductId());
}
}
// 3. Calculate prices
BigDecimal total = calculateTotal(items, products);
// 4. Create order
Order order = new Order(userId, items, total);
order = orderRepository.save(order);
// 5. Reserve inventory
for (OrderItem item : items) {
inventoryService.reserveStock(item.getProductId(), item.getQuantity());
}
// 6. Publish event for other systems
eventPublisher.publishEvent(new OrderCreatedEvent(order));
return order;
}
private BigDecimal calculateTotal(List<OrderItem> items, List<Product> products) {
return items.stream()
.map(item -> {
Product product = products.stream()
.filter(p -> p.getId().equals(item.getProductId()))
.findFirst()
.orElseThrow();
return pricingService.calculatePrice(product, null)
.multiply(BigDecimal.valueOf(item.getQuantity()));
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
// === Event Listeners (handle side effects) ===
@Component
public class OrderEventHandler {
@Autowired private NotificationService notificationService;
@Autowired private AuditService auditService;
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// Send confirmation email (async)
notificationService.sendOrderConfirmation(event.getOrder());
// Log for auditing (async)
auditService.log("Order created: " + event.getOrder().getId());
}
}
This architecture:
- No circular dependencies - Clear dependency flow from top to bottom
- Testable - Each service can be tested independently
- Maintainable - Changes to one service don’t break others
- Scalable - Easy to add new features via events
Prevention: Design Patterns That Avoid Cycles
1. Layered Architecture
Controllers → Application Services → Domain Services → Repositories
Dependencies flow one direction only. Controllers never depend on other controllers, domain services never depend on application services.
2. Dependency Inversion Principle
Depend on abstractions, not concrete classes:
// Don't do this
@Service
public class OrderService {
@Autowired private EmailService emailService; // Concrete dependency
}
// Do this
@Service
public class OrderService {
@Autowired private NotificationChannel notificationChannel; // Interface
}
public interface NotificationChannel {
void notify(String message);
}
@Service
public class EmailService implements NotificationChannel {
@Override
public void notify(String message) {
// Send email
}
}
3. Command/Query Separation
Separate read operations from write operations. Writes can publish events that readers listen to.
Debugging Circular Dependencies
Enable detailed logging:
# application.properties
logging.level.org.springframework.beans.factory.support=DEBUG
This shows you the exact order Spring tries to create beans and where it gets stuck.
Use Spring Boot 2.6+ strict mode (recommended):
spring.main.allow-circular-references=false
This is now the default in Spring Boot 2.6+. It forces you to fix circular dependencies instead of letting Spring try to work around them.
Visualize your dependencies:
Use IDE tools (IntelliJ’s “Show Dependencies”) or plugins like Spring Boot Dependency Analyzer to see your dependency graph visually. Cycles become obvious immediately.
Migration Strategy for Legacy Code
If you inherit code with circular dependencies everywhere:
Step 1: Stop adding new ones
- Review all new code
- Reject PRs with circular dependencies
- Use
allow-circular-references=falsefor new modules
Step 2: Catalog existing cycles
# Find all @Autowired in your codebase
grep -r "@Autowired" src/
Map out which services depend on which.
Step 3: Fix the easiest ones first
- Apply
@Lazyto buy time - Refactor one cycle per sprint
- Start with services that have the fewest dependencies
Step 4: Tackle core services
- Extract shared logic into new services
- Introduce events for cross-cutting concerns
- Add interfaces to break tight coupling
Step 5: Measure progress
// Create a test that fails if cycles exist
@SpringBootTest
public class ArchitectureTest {
@Test
public void shouldNotHaveCircularDependencies() {
// Use ArchUnit or similar library
noClasses()
.should()
.dependOnClassesThat()
.resideInAPackage("..service..")
.check(classes);
}
}
Key Takeaways
- Circular dependencies are a design smell - They indicate poor separation of concerns
- @Lazy is a quick fix, not a solution - Use it temporarily while you refactor
- Extract shared logic - Create orchestration services for operations spanning multiple domains
- Use events for decoupling - Perfect for notifications, auditing, and cross-cutting concerns
- Follow layered architecture - Dependencies should flow in one direction only
- Prevent new cycles - Set
allow-circular-references=falseand review code carefully
When you hit a CircularDependencyException, the stack trace shows you exactly which beans form the cycle. Use Debugly’s trace formatter to quickly parse and analyze Java stack traces. It highlights the dependency chain, making it immediately obvious which services are creating the circular reference. The formatted output helps you decide which dependency to break first.
For more Spring Boot troubleshooting, check out our guide on Spring Boot BeanCreationException, which often appears alongside circular dependency issues.
Now go break those cycles and build better architecture!