Circular Dependency Resolution in Spring Framework: Mechanisms and Best Practices

Dec 03, 2025 · Programming · 9 views · 7.8

Keywords: Spring Framework | Circular Dependency | Bean Injection

Abstract: This article provides an in-depth exploration of how the Spring framework handles circular dependencies between beans. By analyzing Spring's instantiation and injection processes, it explains why BeanCurrentlyInCreationException occurs with constructor injection while setter injection works seamlessly. The core mechanism of Spring's three-level cache for resolving circular dependencies is detailed, along with best practices using the InitializingBean interface for safe initialization. Additionally, performance issues in large-scale projects involving FactoryBeans in circular dependencies are discussed, including solutions such as manual injection via ApplicationContextAware and scenarios for disabling circular reference resolution.

Spring's Circular Dependency Resolution Mechanism

In the Spring framework, circular dependency refers to a situation where two or more beans depend on each other, such as bean A depending on bean B while bean B also depends on bean A. Spring resolves this issue through a clever instantiation and injection sequence, but developers must understand its workings to avoid potential pitfalls.

Analysis of Instantiation and Injection Process

When using setter injection, Spring's resolution process is as follows: First, bean A is instantiated, existing in an "early reference" state where the object is created but properties are not yet injected. Next, bean B is instantiated, also in an early reference state. Then Spring injects bean A into bean B, and finally injects bean B into bean A. The key to this process is that Spring allows injecting references to beans that are not fully initialized into other beans.

// Example: Class A definition
package mypackage;

public class A {
    public A() {
        System.out.println("Creating instance of A");
    }
    
    private B b;
    
    public void setB(B b) {
        System.out.println("Setting property b of A instance");
        this.b = b;
    }
}
// Example: Class B definition
package mypackage;

public class B {
    public B() {
        System.out.println("Creating instance of B");
    }
    
    private A a;
    
    public void setA(A a) {
        System.out.println("Setting property a of B instance");
        this.a = a;
    }
}

Corresponding XML configuration:

<bean id="a" class="mypackage.A">
    <property name="b" ref="b" />
</bean>

<bean id="b" class="mypackage.B">
    <property name="a" ref="a" />
</bean>

The execution output will show:

Creating instance of A
Creating instance of B
Setting property a of B instance
Setting property b of A instance

Limitations of Constructor Injection

Unlike setter injection, constructor injection in circular dependency scenarios leads to BeanCurrentlyInCreationException. This occurs because constructor injection requires all dependencies to be provided immediately when creating the bean instance, which circular dependencies make impossible.

// Problem example: Circular dependency with constructor injection
class A {
    private final B b; // must be initialized in constructor
    public A(B b) { this.b = b; }
}

class B {
    private final A a; // must be initialized in constructor
    public B(A a) { this.a = a; }
}

Safe Initialization with InitializingBean

Since Spring resolves circular dependencies with property injection that may not follow the order in configuration files, critical initialization logic that depends on other properties should not be executed in setter methods. The recommended approach is to implement the InitializingBean interface and perform all initialization in the afterPropertiesSet() method.

import org.springframework.beans.factory.InitializingBean;

public class ExampleBean implements InitializingBean {
    private DependencyBean dependency;
    
    public void setDependency(DependencyBean dependency) {
        this.dependency = dependency;
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        // Execute all initialization logic here
        // Can safely assume all properties have been set
        if (dependency == null) {
            throw new IllegalStateException("dependency property not set");
        }
        dependency.initialize();
    }
}

Performance Issues with FactoryBeans and Circular Dependencies

In large-scale projects, when FactoryBeans participate in circular dependencies, significant performance issues may arise. Spring may attempt to create beans multiple times when resolving such dependencies, leading to FactoryBeanNotInitializedException exceptions and substantial overhead.

Debugging approach: Set a conditional breakpoint in the exception handling section of AbstractBeanFactory.doGetBean():

catch (BeansException ex) {
    // Explicitly remove instance from singleton cache
    destroySingleton(beanName);
    throw ex;
}

Alternative Solutions

For complex circular dependency scenarios, consider the following alternatives:

  1. Manual Injection via ApplicationContextAware: Implement the ApplicationContextAware interface to manually obtain dependent beans in the afterPropertiesSet() method.
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class A implements ApplicationContextAware, InitializingBean {
    private B cyclicDependency;
    private ApplicationContext ctx;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ctx = applicationContext;
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        cyclicDependency = ctx.getBean(B.class);
    }
    
    public void useCyclicDependency() {
        cyclicDependency.doSomething();
    }
}
<ol start="2">
  • Using @Lazy Annotation: For constructor injection with circular dependencies, add the @Lazy annotation to one of the dependencies to delay its resolution.
  • class A {
        private final B b;
        public A(@Lazy B b) { this.b = b; }
    }
    
    class B {
        private final A a;
        public B(A a) { this.a = a; }
    }
    <ol start="3">
  • Disabling Circular Dependency Resolution: For new projects or refactoring scenarios, consider disabling circular dependency resolution to enforce cleaner dependency designs.
  • AbstractRefreshableApplicationContext context = new ClassPathXmlApplicationContext();
    context.setAllowCircularReferences(false);
    context.refresh();

    Conclusion and Recommendations

    Spring elegantly resolves circular dependencies with setter injection through its three-level cache mechanism, but developers should be aware that: 1) constructor injection does not support circular dependencies; 2) property injection order is unpredictable, so critical initialization should be performed in afterPropertiesSet(); 3) FactoryBeans in circular dependencies may cause performance issues; 4) for complex scenarios, consider using ApplicationContextAware, @Lazy annotation, or redesigning dependencies. In large projects, regularly review circular dependencies to ensure they don't impact system performance and maintainability.

    Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.