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.
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
doGetBeanconverts 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?
DefaultSingletonBeanRegistrydefines:
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.postProcessAfterInitializationwhen 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Coder Trainee
Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
