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:

  1. Check the error message for which beans form the cycle
  2. Apply @Lazy to one dependency to break the loop temporarily
  3. Refactor your design—circular dependencies indicate poor separation of concerns
  4. Consider extracting shared logic into a third service
  5. 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:

  • UserService only manages users (no dependency on OrderService)
  • OrderService only manages orders (no dependency on UserService)
  • UserOrderService orchestrates operations that need both
  • Controllers call UserOrderService for 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:

  1. Spring creates UserService with just the UserRepository
  2. Then creates OrderService with the now-complete UserService
  3. 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=false for 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 @Lazy to 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

  1. Circular dependencies are a design smell - They indicate poor separation of concerns
  2. @Lazy is a quick fix, not a solution - Use it temporarily while you refactor
  3. Extract shared logic - Create orchestration services for operations spanning multiple domains
  4. Use events for decoupling - Perfect for notifications, auditing, and cross-cutting concerns
  5. Follow layered architecture - Dependencies should flow in one direction only
  6. Prevent new cycles - Set allow-circular-references=false and 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!