How Spring Boot Instantiates Beans and Resolves Circular Dependencies

This article walks through Spring Boot's bean creation pipeline—from the initial getBean call through doGetBean, the three‑level singleton cache, and the detailed steps of createBean, populateBean, and initializeBean—explaining how circular dependencies are safely resolved and where AOP proxies are generated.

Coder Trainee
Coder Trainee
Coder Trainee
How Spring Boot Instantiates Beans and Resolves Circular Dependencies

1. Starting from getBean

When Spring needs a bean, it ultimately calls getBean(). The internal call chain is:

UserService userService = context.getBean(UserService.class);
// internal call chain
getBean()
    → doGetBean()
    → getSingleton()
    → createBean()
    → doCreateBean()

2. doGetBean – core of bean retrieval

doGetBean

converts the bean name, checks the singleton caches (the key point for circular‑dependency handling), processes @DependsOn, registers dependent beans, and creates the bean if it is not already cached.

protected <T> T doGetBean(String name, @Nullable Class<T> requiredType,
        @Nullable Object[] args, boolean typeCheckOnly) {
    // 1. transform name
    String beanName = transformedBeanName(name);
    // 2. try singleton cache (key for circular‑dependency)
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
        return (T) getObjectForBeanInstance(sharedInstance, name, beanName, null);
    }
    // 3. check parent factory …
    // 4. mark bean as created
    if (!typeCheckOnly) {
        markBeanAsCreated(beanName);
    }
    // 5. obtain merged bean definition
    RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
    // 6. process @DependsOn
    String[] dependsOn = mbd.getDependsOn();
    if (dependsOn != null) {
        for (String dep : dependsOn) {
            registerDependentBean(dep, beanName);
            getBean(dep);
        }
    }
    // 7. create singleton bean
    if (mbd.isSingleton()) {
        sharedInstance = getSingleton(beanName, () -> createBean(beanName, mbd, args));
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
    }
    // 8‑9. prototype and other scopes …
    return (T) bean;
}

3. Three‑level cache – the key to circular‑dependency resolution

What are the three caches?

DefaultSingletonBeanRegistry

defines:

singletonObjects – first‑level cache, stores fully initialized singleton beans (the final product).

earlySingletonObjects – second‑level cache, stores early‑exposed bean instances (half‑finished, before property injection is complete).

singletonFactories – third‑level cache, stores ObjectFactory objects that can create a bean or its proxy on demand.

Why a third cache?

If only the second cache existed, a bean that needs an AOP proxy would either expose the raw instance (losing proxy behavior) or expose a proxy before its dependencies are injected. The third cache holds an ObjectFactory, allowing Spring to defer the decision and return either the raw bean for injection or the proxy for use.

4. getSingleton – complete retrieval logic

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 1. try first‑level cache
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 2. try second‑level cache
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                // 3. try third‑level cache
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

5. When is the third‑level cache populated?

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
    // 1. instantiate bean
    BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
    Object bean = instanceWrapper.getWrappedInstance();
    // 2. early exposure for circular references
    boolean earlySingletonExposure = mbd.isSingleton() && this.allowCircularReferences
            && isSingletonCurrentlyInCreation(beanName);
    if (earlySingletonExposure) {
        addSingletonFactory(beanName,
            () -> getEarlyBeanReference(beanName, mbd, bean));
    }
    // 3. populate properties (Autowired, @Value, etc.)
    populateBean(beanName, mbd, instanceWrapper);
    // 4. initialize bean (Aware, @PostConstruct, init‑methods, AOP)
    Object exposedObject = initializeBean(beanName, bean, mbd);
    // 5. circular‑dependency check – replace with early reference if necessary
    if (earlySingletonExposure) {
        Object earlySingletonReference = getSingleton(beanName, false);
        if (earlySingletonReference != null && exposedObject == bean) {
            exposedObject = earlySingletonReference;
        }
    }
    return exposedObject;
}

6. populateBean – where @Autowired works

protected void populateBean(String beanName, RootBeanDefinition mbd,
        @Nullable BeanWrapper bw) {
    // 1. pre‑instantiation post‑processors
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) {
            if (!bp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
                return;
            }
        }
    }
    // 2. obtain PropertyValues (XML etc.)
    PropertyValues pvs = mbd.hasPropertyValues() ? mbd.getPropertyValues() : null;
    // 3. handle @Autowired / @Value injection (performed by AutowiredAnnotationBeanPostProcessor)
    if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_NAME ||
        mbd.getResolvedAutowireMode() == AUTOWIRE_BY_TYPE) {
        // injection logic executed by post‑processors
    }
    // 4. let post‑processors modify the property values
    for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) {
        pvs = bp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
    }
}

7. initializeBean – Aware, @PostConstruct, AOP

protected Object initializeBean(String beanName, Object bean,
        @Nullable RootBeanDefinition mbd) {
    // 1. invoke Aware callbacks
    if (bean instanceof Aware) {
        if (bean instanceof BeanNameAware) {
            ((BeanNameAware) bean).setBeanName(beanName);
        }
        if (bean instanceof BeanClassLoaderAware) {
            ((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader());
        }
        if (bean instanceof BeanFactoryAware) {
            ((BeanFactoryAware) bean).setBeanFactory(this);
        }
    }
    // 2. BeanPostProcessor before‑initialization
    Object wrappedBean = bean;
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
        wrappedBean = bp.postProcessBeforeInitialization(wrappedBean, beanName);
    }
    // 3. invoke init methods (@PostConstruct, init‑method, etc.)
    invokeInitMethods(beanName, wrappedBean, mbd);
    // 4. BeanPostProcessor after‑initialization – AOP proxy is created here
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
        wrappedBean = bp.postProcessAfterInitialization(wrappedBean, beanName);
    }
    return wrappedBean;
}

8. AOP proxy generation timing

The proxy is created in

AnnotationAwareAspectJAutoProxyCreator.postProcessAfterInitialization

when the bean is not an early proxy reference.

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}

9. Frequently asked interview questions

How does Spring solve circular dependencies?

Spring uses the three‑level cache:

First‑level cache holds fully initialized beans.

Second‑level cache holds early‑exposed half‑finished beans.

Third‑level cache holds ObjectFactory that can produce either the raw bean or its proxy.

Typical scenario (A depends on B, B depends on A):

Instantiate A, put its ObjectFactory into the third‑level cache.

During A’s property injection, discover dependency on B and create B.

Instantiate B, put its ObjectFactory into the third‑level cache.

When B needs A, retrieve A from the third‑level cache, obtain the early reference, and inject it into B.

B finishes initialization and moves to the first‑level cache.

Resume A, now B is ready, inject it, and A completes initialization.

Why can’t constructor injection solve circular dependencies?

Constructor injection requires all dependencies to be available before the bean instance is created. In a circular reference the dependent bean does not exist yet, so the constructor cannot be satisfied. Property or setter injection (via @Autowired) allows early exposure of a bean reference, enabling the circular reference to be broken.

Why is the third‑level cache necessary?

Without it, a bean that later needs an AOP proxy would either expose the raw instance (losing proxy behavior) or expose a proxy before its dependencies are injected, breaking the injection contract. The ObjectFactory in the third cache defers the decision until after dependency injection, ensuring both correct injection and proper proxy creation.

10. Visual summary

A textual flow diagram illustrates the full bean creation process from getBean down to the final placement in the first‑level cache, highlighting where the three caches are consulted and where AOP proxies are generated.

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.

aopSpringspring-bootdependency injectionbeancircular-dependencybean-lifecycle
Coder Trainee
Written by

Coder Trainee

Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.

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.