Hello! Welcome back to your interview preparation course.
In our last lesson on Aspect-Oriented Programming (AOP), we saw how Spring creates dynamic proxies to wrap your beans and inject cross-cutting concerns like logging. This ability to intercept and manage bean creation is a powerful feature of the Spring IoC container, but it also introduces complexities. Today, we'll explore one of the most classic interview problems related to the bean lifecycle: circular dependencies.
Your learning outcome for this lesson is to solve a circular dependency problem in a Spring application and explain the underlying cause. Understanding this topic in depth is a hallmark of a senior developer, as it proves you have a solid grasp of how the Spring container works internally.
1. What is a Circular Dependency?
A circular dependency occurs when two or more beans depend on each other, forming a closed loop. The simplest case is when Bean A depends on Bean B, and Bean B depends on Bean A.

This situation creates a "chicken-and-egg" problem for the Spring container: to create A, it needs B, but to create B, it needs A. How the container handles this depends entirely on how the dependencies are injected.
To start, let's read a clear definition of the problem and see what happens in the most problematic scenario: constructor injection.
Circular Dependencies in Spring
The article 'Circular Dependencies in Spring' from Baeldung provides a concise introduction to the concept and immediately demonstrates the problem using constructor injection.
Please read the first three sections: '1. What Is a Circular Dependency?', '2. What Happens in Spring', and '3. A Quick Example'. Focus on understanding the definition and why constructor injection leads to a BeanCurrentlyInCreationException.
As the article explains, when you use constructor injection, Spring must resolve all constructor arguments before it can create the bean instance. In a circular dependency, this is impossible. Spring will try to create CircularDependencyA, see that it needs CircularDependencyB, then try to create CircularDependencyB, see that it needs CircularDependencyA, and realize it's in an unresolvable loop. At this point, it gives up and throws a BeanCurrentlyInCreationException.
2. How Spring Solves (Some) Circular Dependencies
While constructor injection fails, you might have noticed that circular dependencies with field injection (@Autowired on a field) or setter injection often work without any errors. This isn't magic; it's a deliberate and clever design within the Spring container.
Spring can resolve circular dependencies for singleton-scoped beans by using a three-level cache system. This mechanism allows a partially initialized bean (an "early reference") to be exposed to another bean before its own dependencies are fully injected.
This is the deep technical part that interviewers love to probe. Let's dive into how it works.
The Three-Level Cache
During bean creation, Spring uses three maps to keep track of singleton beans:
-
Level 1 Cache:
singletonObjects- This is the main cache. It stores fully created, initialized, and populated beans that are ready for use.
- Type:
Map<String, Object>
-
Level 2 Cache:
earlySingletonObjects- This cache stores "early references" to beans. These are bean instances that have been created but have not yet had their properties set or initialization methods (
@PostConstruct) called. - Type:
Map<String, Object>
- This cache stores "early references" to beans. These are bean instances that have been created but have not yet had their properties set or initialization methods (
-
Level 3 Cache:
singletonFactories- This is the most critical cache for solving the problem. It stores
ObjectFactoryinstances. AnObjectFactoryis a functional interface that knows how to create an early reference to a bean, and importantly, can create a proxy if needed for AOP. - Type:
Map<String, ObjectFactory<?>>
- This is the most critical cache for solving the problem. It stores
The Step-by-Step Process
Let's trace how Spring uses these caches to resolve the ServiceA -> ServiceB -> ServiceA cycle with setter injection:
getBean("serviceA")is called.- Spring finds no existing instance of
serviceAin any cache. It begins creating it. - Spring instantiates
serviceA(calls its constructor) but doesn't inject dependencies yet. - Crucially, Spring creates an
ObjectFactoryforserviceAand puts it into the Level 3 cache (singletonFactories). This factory knows how to return an early reference to the rawserviceAobject. - Spring starts populating
serviceA's properties and discovers it needsserviceB. getBean("serviceB")is called. Spring starts creatingserviceB(repeating steps 2-4 forserviceB). It instantiatesserviceBand puts a factory for it in the Level 3 cache.- Spring starts populating
serviceB's properties and discovers it needsserviceA. getBean("serviceA")is called again. This time, Spring checks the caches:- Level 1 (
singletonObjects): Empty forserviceA. - Level 2 (
earlySingletonObjects): Empty forserviceA. - Level 3 (
singletonFactories): Found! It finds theObjectFactoryforserviceA.
- Level 1 (
- Spring invokes the factory from Level 3. The factory produces an early reference to the
serviceAobject and puts this reference into the Level 2 cache (earlySingletonObjects). The factory is then removed from Level 3. - This early
serviceAreference is injected intoserviceB. serviceBis now fully initialized. It's moved from the early cache to the Level 1 cache (singletonObjects).- The container returns to finishing
serviceA. It now has the fully-builtserviceBinstance and injects it intoserviceA. serviceAis now fully initialized and is placed in the Level 1 cache. The cycle is resolved.
To explore this mechanism in greater detail, the following resource provides a thorough explanation.
The article 'Spring Circular Dependency' from Alibaba Cloud offers a very deep and technical breakdown of the three-level cache system. It explains exactly why this design is necessary.
Please read section '5. Level-3 Cache to Resolve Circular Dependencies' and '6. Why Doesn't Spring Use the Level-1 and Level-2 Cache to Solve Circular Dependency?'. Focus on understanding the role of each of the three caches. Pay special attention to why the third level (the singletonFactories) is necessary, especially in the context of AOP.
Test your understanding!
Based on the readings, why is the third-level cache (singletonFactories) crucial for applications using Spring AOP? Why not just use two caches (singletonObjects and earlySingletonObjects)?
Show answer
The third-level cache, which stores ObjectFactory instances, is essential for handling AOP proxies.
If a bean involved in a circular dependency (like serviceA) also needs to be proxied (e.g., for @Transactional or a custom aspect), the dependency (serviceB) must receive a reference to the final proxy, not the raw object.
The ObjectFactory in the Level 3 cache allows Spring to delay the decision of whether to create a proxy. The factory is only invoked when a circular dependency is detected. At that moment, it runs post-processors that can wrap the raw object in a proxy. This proxy is then put into the Level 2 cache and injected.
Without the third level, Spring would have only two choices, both bad:
- Expose the raw, un-proxied object as the early reference. This would break AOP, as the other bean would hold a reference to the raw object, bypassing any advice.
- Create proxies for every bean upfront, just in case. This would be very inefficient.
The factory approach provides a "just-in-time" proxy creation mechanism, making it both efficient and correct.
3. When Spring Fails: The Edge Cases
Spring's caching mechanism is robust, but it's not a silver bullet. Knowing when it fails is just as important as knowing how it works.
- Constructor Injection: As we've established, this is the primary failure case. A bean's constructor must complete before an early reference can be created, which is impossible in a circular dependency.
@PrototypeScoped Beans: The entire resolution mechanism relies on caching singletons. Prototype beans are designed to be new instances every time they're requested, so Spring doesn't cache them and cannot provide an early reference.@AsyncMethods: A circular dependency involving a bean with@Asyncmethods can sometimes fail. This is because the post-processor that enables asynchronicity might wrap the bean in a proxy. If this happens late in the bean's lifecycle, the early reference that was already injected might not be the same as the final, proxied bean, causing Spring to throw an error to enforce the singleton contract. The Alibaba Cloud resource you read (section 7) touches on this.
4. How to Solve Circular Dependency Problems
In an interview, after explaining the "how," you must provide practical solutions. Here they are, in order of preference.
-
Redesign (The Best Solution)
A circular dependency is often a code smell indicating that your classes are trying to do too much or that responsibilities are not clearly separated. The best solution is to refactor your code.- Extract a third bean: Create a new bean that holds the common logic that both
ServiceAandServiceBneed, and have them both depend on this new bean. - Use an interface: If
ServiceAonly needs a small part ofServiceB's functionality, define that in an interface.ServiceBcan implement it, butServiceAdepends only on the interface, potentially breaking the cycle if the implementation can be provided by another bean.
- Extract a third bean: Create a new bean that holds the common logic that both
-
Use
@Lazy
If you cannot redesign, using@Lazyis a clean and explicit way to break the cycle. By adding@Lazyto an@Autowiredfield or a constructor parameter, you tell Spring to inject a proxy of the dependency instead of the actual bean. The actual bean will only be requested from the container the first time one of its methods is called. This defers initialization and breaks the circular dependency loop during startup.@Component public class ServiceA { // Spring injects a lazy-initialized proxy for ServiceB private final ServiceB serviceB; @Autowired public ServiceA(@Lazy ServiceB serviceB) { this.serviceB = serviceB; } } -
Switch to Setter or Field Injection
If your design is locked into constructor injection, the simplest fix is to refactor one of the beans to use setter or field injection. This allows Spring's three-level cache mechanism to kick in and resolve the cycle.// Before: Constructor Injection @Component public class ServiceA { private final ServiceB serviceB; public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; } } // After: Setter Injection @Component public class ServiceA { private ServiceB serviceB; @Autowired public void setServiceB(ServiceB serviceB) { this.serviceB = serviceB; } }
For a summary of these practical workarounds, you can refer back to the Baeldung article.
Circular Dependencies in Spring
The Baeldung article provides clear code examples for the most common solutions you'd propose in an interview.
Review sections '4.1. Redesign', '4.2. Use @Lazy', and '4.3. Use Setter/Field Injection'. These are the top three solutions you should have ready for an interview.
Conclusion
You've now done a deep dive into one of Spring's most intricate internal mechanisms. In a senior-level interview, being able to articulate this topic clearly demonstrates a true understanding of the framework beyond just using its features.
Key Takeaways:
- The Problem: Circular dependencies create an unresolvable "chicken-and-egg" situation during bean creation.
- The Failure: Spring unequivocally fails to resolve circular dependencies with constructor injection, throwing a
BeanCurrentlyInCreationException. - The Solution: For singleton beans with setter/field injection, Spring uses a three-level cache (
singletonObjects,earlySingletonObjects,singletonFactories) to expose an early reference to a bean before it's fully initialized. - The AOP Connection: The third-level cache (
singletonFactories) is critical for correctly applying AOP proxies in circular dependency scenarios. - Your Strategy: The best way to "solve" a circular dependency is to redesign your components. If that's not possible, use
@Lazyor switch to setter injection as pragmatic workarounds.
Preview of the Next Lesson:
We've just explored the lifecycle of beans within the Spring container. Now, we'll shift our focus from the application container to the database. In the next lesson, we will begin our module on Spring Data & Transaction Management by examining the lifecycle of data itself. You will explain the JPA entity lifecycle and the role of the persistence context, which is fundamental to understanding how Spring manages your data's state from creation to deletion.