Why @Async Triggers Circular Dependency Errors in Spring and How to Resolve Them

This article explains why Spring cannot resolve circular dependencies when a bean method is annotated with @Async, detailing the underlying bean post‑processor mechanisms, the order of proxy creation, and practical solutions such as removing @Async, using @Lazy, or adjusting bean factory settings.

Shepherd Advanced Notes
Shepherd Advanced Notes
Shepherd Advanced Notes
Why @Async Triggers Circular Dependency Errors in Spring and How to Resolve Them

Background

A colleague reported that her Spring project failed to start. The console showed a BeanCurrentlyInCreationException for bean 'AService', indicating a circular reference problem.

Exception in thread "main" org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AService': Bean with name 'AService' has been injected into other beans [BService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over‑eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:602)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractBeanFactory.java:495)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$(AbstractBeanFactory.java:317)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)

The exception suggested a circular dependency, yet Spring normally resolves such cases. The colleague had added @Async to a method, which triggered the issue.

@Async Annotation Mechanics

The @Async annotation is processed by AsyncAnnotationBeanPostProcessor, which is registered in the container by @EnableAsync. This post‑processor implements BeanPostProcessor and intercepts beans after initialization.

AsyncAnnotationBeanPostProcessor

It overrides postProcessAfterInitialization to create a dynamic proxy for beans whose methods carry @Async. The proxy creation is guarded by isEligible(bean, beanName), which checks for the annotation.

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
    if (this.advisor == null || bean instanceof AopInfrastructureBean) {
        // Ignore AOP infrastructure such as scoped proxies.
        return bean;
    }
    if (bean instanceof Advised) {
        Advised advised = (Advised) bean;
        if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {
            // Add our local Advisor to the existing proxy's Advisor chain...
            if (this.beforeExistingAdvisors) {
                advised.addAdvisor(0, this.advisor);
            } else {
                advised.addAdvisor(this.advisor);
            }
            return bean;
        }
    }
    if (isEligible(bean, beanName)) {
        ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
        if (!proxyFactory.isProxyTargetClass()) {
            evaluateProxyInterfaces(bean.getClass(), proxyFactory);
        }
        proxyFactory.addAdvisor(this.advisor);
        customizeProxyFactory(proxyFactory);
        return proxyFactory.getProxy(getProxyClassLoader());
    }
    // No proxy needed.
    return bean;
}

Thus, after a bean’s initialization phase, AsyncAnnotationBeanPostProcessor may replace the original instance with a proxy.

AOP Implementation

AOP in Spring also relies on dynamic proxies, but it is driven by AnnotationAwareAspectJAutoProxyCreator, which also implements BeanPostProcessor and its own postProcessAfterInitialization method.

@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (!this.earlyProxyReferences.contains(cacheKey)) {
            // generate dynamic proxy if needed
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}

Both mechanisms create proxies after bean initialization, but they use different post‑processor classes.

How Spring Resolves Circular Dependencies

Spring solves circular dependencies using a three‑level cache. During bean creation, an ObjectFactory for the early reference is stored, allowing other beans to obtain a partially constructed instance.

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

The early reference is obtained via getEarlyBeanReference, which delegates to all registered SmartInstantiationAwareBeanPostProcessor implementations, including AnnotationAwareAspectJAutoProxyCreator.

protected Object getEarlyBeanReference(Object bean, String beanName, ObjectFactory<?> singletonFactory) {
    // ... invoke SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference
    return exposedObject;
}

Why @Async Breaks Circular Dependency Resolution

When AService (annotated with @Async) depends on BService, and BService depends back on AService, Spring first creates an early reference for AService via AnnotationAwareAspectJAutoProxyCreator. This early reference may be a proxy, but it is **not** processed by AsyncAnnotationBeanPostProcessor because that processor does not implement SmartInstantiationAwareBeanPostProcessor.

After both beans are instantiated, the container invokes post‑processors in order: first AnnotationAwareAspectJAutoProxyCreator, then AsyncAnnotationBeanPostProcessor. The latter creates a new proxy for AService, resulting in two different objects – the early reference and the final bean. Spring detects this mismatch and throws BeanCurrentlyInCreationException.

if (earlySingletonExposure) {
    Object earlySingletonReference = getSingleton(beanName, false);
    if (earlySingletonReference != null) {
        if (exposedObject == bean) {
            // no proxy created – keep early reference
            exposedObject = earlySingletonReference;
        } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
            // proxy created – raise exception
            throw new BeanCurrentlyInCreationException(beanName, ...);
        }
    }
}

Therefore, the presence of @Async causes the final bean to differ from the early exposed reference, and Spring cannot reconcile them.

How to Resolve the Issue

Several approaches can avoid the exception:

Refactor the design to eliminate circular dependencies.

Remove @Async and implement asynchronous behavior manually with a thread pool.

Mark the injected field with @Lazy to defer resolution.

@Component
public class AService {
    @Resource
    @Lazy
    private BService bService;

    @Async
    public void save() { }
}

Set allowRawInjectionDespiteWrapping to true via a custom BeanFactoryPostProcessor.

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        ((DefaultListableBeanFactory) beanFactory).setAllowRawInjectionDespiteWrapping(true);
    }
}

While this suppresses the exception, it is not recommended because it permits mismatched bean instances and may skip necessary proxying.

Choosing the appropriate solution depends on the project’s constraints and the need for asynchronous execution.

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.

proxyAOPSpringasynccircular dependencyBeanPostProcessor
Shepherd Advanced Notes
Written by

Shepherd Advanced Notes

Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.

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.