Resolving Circular Dependencies Between Spring Beans Using Constructor Injection
Xuan

Xuan @xuan_56087d315ff4f52254e6

About: Software Engineer

Joined:
Aug 8, 2025

Resolving Circular Dependencies Between Spring Beans Using Constructor Injection

Publish Date: Aug 8
0 0

Resolving Circular Dependencies Between Spring Beans Using Constructor Injection

Circular dependencies can be tricky situations in Spring applications. They happen when two or more beans directly or indirectly depend on each other. This can lead to issues during application startup, preventing your beans from being properly initialized. This post explores how to tackle circular dependencies, specifically when using constructor injection, and provides practical solutions.

Understanding Circular Dependencies

Imagine two classes, BeanA and BeanB. BeanA needs an instance of BeanB to work correctly, and BeanB needs an instance of BeanA.

 public class BeanA {
  private final BeanB beanB;

  public BeanA(BeanB beanB) {
  this.beanB = beanB;
  }
 }

 public class BeanB {
  private final BeanA beanA;

  public BeanB(BeanA beanA) {
  this.beanA = beanA;
  }
 }
Enter fullscreen mode Exit fullscreen mode

If Spring tries to create BeanA, it first needs to create BeanB. But to create BeanB, it needs BeanA. This creates a loop, a circular dependency.

The Problem with Constructor Injection and Circular Dependencies

Constructor injection, generally considered best practice for dependency injection, becomes problematic with circular dependencies. Spring cannot fully construct either bean because each requires the other to be fully constructed first. This results in a BeanCurrentlyInCreationException during application startup.

Why Circular Dependencies are Bad

Circular dependencies can cause several problems:

  • Startup Failure: Your application might fail to start because Spring can't resolve the dependencies.
  • Unexpected Behavior: Even if the application starts, the beans might not be fully initialized, leading to unexpected behavior or errors.
  • Code Complexity: Circular dependencies often indicate a design flaw, making your code harder to understand and maintain.

Breaking the Cycle: Solutions

Here are several strategies to resolve circular dependencies with constructor injection:

1. Rethink Your Design

The best solution is often to refactor your code to eliminate the circular dependency. Ask yourself:

  • Is the dependency truly necessary? Could one bean function without the other?
  • Can the shared functionality be moved to a separate class? Extracting the common logic into a new bean can break the cycle.
  • Can one bean depend on an interface instead of a concrete class? This can loosen the coupling and potentially break the dependency.

For example, instead of BeanA and BeanB directly depending on each other, they could both depend on an interface CommonInterface implemented by a CommonService.

 public interface CommonInterface {
  void doSomething();
 }

 @Service
 public class CommonService implements CommonInterface {
  @Override
  public void doSomething() {
  // Implementation
  }
 }

 @Service
 public class BeanA {
  private final CommonInterface commonInterface;

  public BeanA(CommonInterface commonInterface) {
  this.commonInterface = commonInterface;
  }
 }

 @Service
 public class BeanB {
  private final CommonInterface commonInterface;

  public BeanB(CommonInterface commonInterface) {
  this.commonInterface = commonInterface;
  }
 }
Enter fullscreen mode Exit fullscreen mode

This approach removes the direct dependency between BeanA and BeanB, resolving the circular dependency.

2. Setter Injection (Use Sparingly)

While constructor injection is preferred, setter injection can sometimes be used to break a circular dependency. With setter injection, Spring can create the beans first and then set the dependencies later.

 public class BeanA {
  private BeanB beanB;

  public BeanA() {}

  @Autowired
  public void setBeanB(BeanB beanB) {
  this.beanB = beanB;
  }
 }

 public class BeanB {
  private BeanA beanA;

  public BeanB() {}

  @Autowired
  public void setBeanA(BeanA beanA) {
  this.beanA = beanA;
  }
 }
Enter fullscreen mode Exit fullscreen mode

Important Considerations:

  • Optional Dependency: Setter injection makes the dependency optional. beanB in BeanA and beanA in BeanB can be null if not injected.
  • Mutability: Setter injection introduces mutability, which can make your code harder to reason about.

This approach should be used as a last resort when refactoring is not feasible. Prefer constructor injection whenever possible.

3. @Lazy Annotation (Carefully)

The @Lazy annotation tells Spring to delay the initialization of a bean until it is actually needed. This can be used to break a circular dependency by postponing the creation of one of the beans involved.

 @Service
 public class BeanA {
  private final BeanB beanB;

  public BeanA(@Lazy BeanB beanB) {
  this.beanB = beanB;
  }
 }

 @Service
 public class BeanB {
  private final BeanA beanA;

  public BeanB(BeanA beanA) {
  this.beanA = beanA;
  }
 }
Enter fullscreen mode Exit fullscreen mode

In this example, BeanB is created immediately, but BeanA is only created when it is first accessed through beanB.

Important Considerations:

  • Performance Impact: Lazy initialization can introduce a slight performance overhead when the bean is first accessed.
  • Potential for Runtime Errors: Errors related to the delayed bean creation might occur later in the application's lifecycle, making them harder to debug.
  • Doesn't fix the underlying problem: @Lazy annotation delays the exception, it doesn't solve the problem.

4. ApplicationContextAware (Avoid if Possible)

This approach involves accessing the Spring ApplicationContext directly within your bean to retrieve dependencies.

 @Service
 public class BeanA implements ApplicationContextAware {

  private ApplicationContext applicationContext;
  private BeanB beanB;

  public BeanA() {}

  public BeanB getBeanB() {
  if(beanB == null) {
  beanB = applicationContext.getBean(BeanB.class);
  }
  return beanB;
  }

  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  this.applicationContext = applicationContext;
  }
 }

 @Service
 public class BeanB {
  private final BeanA beanA;

  public BeanB(BeanA beanA) {
  this.beanA = beanA;
  }
 }
Enter fullscreen mode Exit fullscreen mode

Why to Avoid:

  • Tight Coupling: This tightly couples your bean to the Spring container, making it harder to test and reuse outside of a Spring environment.
  • Violation of Inversion of Control: You are explicitly retrieving dependencies instead of having them injected.

This approach should be avoided whenever possible. It's generally considered an anti-pattern.

Best Practices

  • Prioritize Refactoring: Always try to refactor your code to eliminate circular dependencies. A well-designed application should minimize or avoid them entirely.
  • Prefer Constructor Injection: Use constructor injection as your primary method for dependency injection.
  • Use Setter Injection with Caution: Only use setter injection as a last resort when refactoring is not feasible and you understand the implications.
  • Be Mindful of @Lazy: Use @Lazy carefully and be aware of the potential performance and debugging challenges.
  • Avoid ApplicationContextAware: Steer clear of using ApplicationContextAware due to its tight coupling and violation of IoC principles.

Conclusion

Circular dependencies can be a pain, but understanding the causes and available solutions can help you resolve them effectively. Remember that the best approach is often to refactor your code to eliminate the dependency altogether. When that's not possible, use @Lazy and setter injection sparingly and with careful consideration. By following these guidelines, you can build robust and maintainable Spring applications.

Comments 0 total

    Add comment