Resolving AOP + IOC Circular Dependency Conflicts in Spring

The article explains why Spring's three‑level cache solves simple circular dependencies but fails for AOP‑enhanced beans, describes the timeline mismatch that causes BeanCurrentlyInCreationException, and presents tiered solutions including @Lazy injection, setter refactoring, early proxy processing, and architectural redesign.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
Resolving AOP + IOC Circular Dependency Conflicts in Spring

1. Spring IOC Three‑Level Cache Principle

1.1 Three‑Level Cache Definition

Spring singleton beans are stored in three caches: singletonObjects (fully initialized beans), earlySingletonObjects (instantiated but not yet populated beans), and singletonFactories (ObjectFactory instances that can create early proxies).

// 一级缓存:完全初始化完毕、属性填充、代理完成、可直接使用的【成品Bean】
private final Map<String, Object> singletonObjects;

// 二级缓存:实例化完成、属性未填充、未初始化、未代理的【半成品裸Bean】
private final Map<String, Object> earlySingletonObjects;

// 三级缓存:Bean工厂对象,用于【提前生成代理对象】的函数工厂
private final Map<String, ObjectFactory<?>> singletonFactories;

1.2 Why the Three‑Level Cache Is Needed

Second‑level cache can only store fixed objects.

Third‑level cache stores object factories (lambdas) that can execute logic dynamically.

For a normal bean the factory returns the raw object.

For an AOP‑proxied bean the factory should return a proxy, but Spring does not support this natively.

Without the third‑level cache, AOP‑enhanced beans always cause circular‑dependency failures.

1.3 Normal Circular Dependency Flow (No AOP)

Create A (new A()).

Put A into third‑level cache (expose factory).

Populate A's properties, discover dependency on B, start creating B.

Create B and put it into third‑level cache.

Populate B's properties, discover dependency on A.

Retrieve A's factory from third‑level cache, obtain the half‑finished A and place it into second‑level cache.

Inject half‑finished A into B, finish B initialization, move B to first‑level cache.

Inject fully initialized B into A, finish A initialization, move A to first‑level cache.

Application starts successfully.

Key point: For ordinary beans the half‑finished raw object and the final product are identical, so the process is safe.

2. AOP Proxy Breaks the Three‑Level Cache

2.1 When Spring Creates an AOP Proxy

Spring creates the proxy in the postProcessAfterInitialization phase, i.e., after property population and init method execution.

Circular‑dependency early exposure happens before property population, so the proxy creation timing is mismatched.

Result: Timeline mismatch leads to failure.

2.2 Full Timeline of AOP Circular‑Dependency Error

Instantiate A and put it into third‑level cache.

Populate A's properties, need to create B.

Instantiate B, its properties need A.

Attempt to retrieve A's factory from third‑level cache for early exposure.

At this moment A is not yet initialized, so AOP proxy cannot be created; only the raw object is returned.

B receives the raw A, finishes B initialization.

A continues initialization, post‑processors run, and the AOP proxy is finally generated.

The container now holds two A instances: a raw A inside B and a proxied A exposed externally.

Spring detects the inconsistency and throws BeanCurrentlyInCreationException.

2.3 Why Spring Chooses to Fail

If Spring allowed the mismatch, hidden production bugs would appear: transaction loss, intermittent AOP behavior, and impossible debugging.

Therefore Spring prefers to abort startup with a clear exception.

3. Different Proxy Types and Injection Methods

3.1 JDK Dynamic Proxy vs CGLIB

JDK proxy (interface‑based): raw object and proxy have different types – 100% conflict.

CGLIB proxy (subclass‑based): types match but object addresses differ – still conflict.

3.2 Injection‑Method Conflict Summary

Field @Autowired : normal beans compatible; AOP beans cause errors (early raw object exposure).

Setter injection : same behavior as field injection.

Constructor injection : completely unsupported for circular dependencies.

@Lazy delayed injection : temporarily compatible by breaking the creation chain.

3.3 Multi‑AOP Stacking Issues

Longer proxy chain and more post‑processor execution.

Higher probability of early‑proxy failure.

Spring more likely to detect object inconsistency and raise an error.

4. Hidden Production Bugs Caused by Circular Dependencies

4.1 Transaction Failure Bug

B receives the raw A object; when A's @Transactional method is invoked, no proxy is present, so the transaction is completely ineffective, leading to dirty reads, overselling, and data inconsistency.

4.2 Random AOP Feature Loss

Controller → A (proxy) works, but internal B → A uses the raw object, so logging, rate‑limit, and permission checks are skipped.

4.3 Sporadic Runtime Anomalies

Different bean loading orders cause the problem to appear intermittently, making it extremely hard to trace.

5. Tiered Solutions

Solution 1 – @Lazy Delayed Injection (Emergency Fix)

Underlying principle

@Lazy injects a delayed‑proxy factory instead of the real bean, cutting the circular creation chain.

Usage

@Service
public class UserService {
    @Autowired
    @Lazy
    private OrderService orderService;
}

@Service
public class OrderService {
    @Autowired
    private UserService userService;
}

Pros / Cons

✅ Fast, zero code change, suitable for urgent fixes.

❌ Does not address the root cause; dual‑object risk remains in complex scenarios.

Solution 2 – Unified Setter Injection

Replace constructor injection with setter injection (optionally combined with @Lazy) to lower the probability of circular‑dependency conflicts.

@Service
public class UserService {
    private OrderService orderService;

    @Autowired
    public void setOrderService(@Lazy OrderService orderService) {
        this.orderService = orderService;
    }
}

Solution 3 – Early AOP Proxy Processor (Core Idea)

Core idea

Implement a SmartInstantiationAwareBeanPostProcessor that wraps beans with an AOP proxy during the early‑exposure phase, so the object placed in the third‑level cache is already a proxy.

import org.springframework.aop.framework.autoproxy.AnnotationAwareAspectJAutoProxyCreator;
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class EarlyAopProxyProcessor implements SmartInstantiationAwareBeanPostProcessor {
    private final AnnotationAwareAspectJAutoProxyCreator proxyCreator;

    public EarlyAopProxyProcessor(AnnotationAwareAspectJAutoProxyCreator proxyCreator) {
        this.proxyCreator = proxyCreator;
    }

    // Key: directly wrap the bean when it is exposed early
    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) {
        // Force early AOP proxy, solving proxy‑inconsistency in circular dependencies
        return proxyCreator.wrapIfNecessary(bean, beanName, bean);
    }
}

Effect

The early‑exposed object in the third‑level cache is already a proxy.

B receives the final proxied A.

All AOP features (transaction, rate‑limit, logging) work 100%.

Circular‑dependency conflicts are completely eliminated.

Solution 4 – Architectural Refactor

Treat circular dependency as an architectural defect. Recommended actions:

Extract common logic into a separate service to break bidirectional coupling.

Enforce one‑way dependencies (upper layers depend on lower layers only).

Replace synchronous calls with Spring events or message‑queue decoupling.

Move stateless utility logic out of the IoC container.

6. Special Scenarios

6.1 Constructor Injection + AOP (No Fix)

Constructor injection occurs before bean instantiation, while the three‑level cache stores after instantiation. The timelines are completely misaligned, leaving no compatible solution; code must be refactored.

6.2 Prototype Beans + AOP

The three‑level cache only applies to singleton beans. Prototype beans are created anew each time, so circular dependencies always explode.

6.3 SpringBoot 2.x vs 3.x

SpringBoot 2.x: circular dependencies are allowed by default; AOP scenarios trigger errors.

SpringBoot 3.x: circular dependencies are disabled by default, causing immediate failure regardless of AOP.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

proxyAOPIOCSpringBeancircular dependencyLazy injection
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.