Explain different ways provided by Spring Boot to resolve circular dependencies.

Circular dependency occurs when two or more Spring beans depend on each other directly or indirectly, creating a dependency cycle that prevents proper bean creation. Spring Boot provides several ways to resolve such issues, but the preferred solution is to redesign the application to reduce tight coupling between components.

Key Points: • The best solution is to refactor the design and eliminate circular dependencies. • Spring provides mechanisms such as @Lazy and setter injection to break dependency cycles. • Constructor-based circular dependencies usually fail during application startup.

Ways to Resolve Circular Dependencies:

1. Redesign the Application (Recommended)

• Extract common functionality into a separate service. • Follow Single Responsibility Principle (SRP). • Reduce tight coupling between beans.

Example:

Before: OrderService → PaymentService PaymentService → OrderService

After: OrderService → TransactionService PaymentService → TransactionService

This removes the circular reference completely.

2. Use @Lazy Annotation

• Delays bean initialization until it is actually required. • Spring injects a proxy object instead of the real bean.

Code Example:

@Service
public class OrderService {

    private final PaymentService paymentService;

    public OrderService(
            @Lazy PaymentService paymentService) {

        this.paymentService = paymentService;
    }
}

@Service
public class PaymentService {

    private final OrderService orderService;

    public PaymentService(
            @Lazy OrderService orderService) {

        this.orderService = orderService;
    }
}

3. Use Setter Injection

• Spring creates bean instances first. • Dependencies are injected afterward. • Can resolve some circular dependency scenarios.

Code Example:

@Service
public class OrderService {

    private PaymentService paymentService;

    @Autowired
    public void setPaymentService(
            PaymentService paymentService) {

        this.paymentService = paymentService;
    }
}

4. Use Field Injection

• Spring injects dependencies after bean creation. • Can resolve some cycles but is generally not recommended because of poor testability.

5. Use ObjectProvider

• Provides lazy access to beans. • Bean is created only when requested.

Code Example:

@Autowired
private ObjectProvider<PaymentService>
        paymentServiceProvider;

public void process() {

    PaymentService paymentService =
            paymentServiceProvider.getObject();
}

6. Use ApplicationContext

• Retrieve beans programmatically when required. • Useful in rare advanced scenarios.

Code Example:

@Autowired
private ApplicationContext context;

public void process() {

    PaymentService paymentService =
            context.getBean(
                    PaymentService.class);
}

7. Event-Driven Communication

• Replace direct dependencies using Spring Events. • Promotes loose coupling between services.

Example: OrderService publishes an event. PaymentService listens to the event.

This completely removes direct bean dependencies.

Comparison:

• Redesign Architecture - Best Practice - Recommended

• @Lazy - Quick Fix - Commonly Used

• Setter Injection - Works for Some Cases - Acceptable

• ObjectProvider - Lazy Resolution - Good Option

• ApplicationContext - Advanced Usage - Use Sparingly

• Event-Based Design - Loose Coupling - Excellent for Large Systems

Interview Tip: A concise interview answer is: Spring Boot resolves circular dependencies using approaches such as @Lazy, setter injection, ObjectProvider, and event-driven communication. However, the preferred solution is to redesign the application and remove tight coupling between beans. Among the available options, @Lazy is the most commonly used quick fix, while architectural refactoring is the best long-term solution.