The IoC Container Internals: BeanFactory Hierarchy and Bean Creation Pipeline
Prerequisites
- ›Familiarity with Spring's @Bean/@Component from a user perspective
- ›Basic understanding of design patterns (Template Method, Factory)
The IoC Container Internals: BeanFactory Hierarchy and Bean Creation Pipeline
At the heart of every Spring application lies an IoC container — a subsystem responsible for creating, configuring, wiring, and managing the lifecycle of every object in your application. From the outside, it looks simple: you call getBean() and get an object back. On the inside, it's a deeply layered architecture built on extreme interface segregation, the Template Method pattern, and a sophisticated caching system for resolving circular dependencies. This article maps the entire structure.
The Interface Pyramid: Read vs Write vs Listable
Spring's container is defined through an inheritance hierarchy of interfaces that separates concerns with surgical precision. At the root sits BeanFactory — the most basic client interface:
spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java#L123-L156
BeanFactory provides getBean(), containsBean(), isSingleton() and similar read-only operations. That's all most application code should ever need. The hierarchy then branches:
classDiagram
class BeanFactory {
<<interface>>
+getBean(String name) Object
+containsBean(String name) boolean
+isSingleton(String name) boolean
}
class ListableBeanFactory {
<<interface>>
+getBeanDefinitionNames() String[]
+getBeansOfType(Class) Map
}
class HierarchicalBeanFactory {
<<interface>>
+getParentBeanFactory() BeanFactory
+containsLocalBean(String) boolean
}
class ConfigurableBeanFactory {
<<interface>>
+addBeanPostProcessor(BeanPostProcessor)
+setParentBeanFactory(BeanFactory)
+registerScope(String, Scope)
}
class AutowireCapableBeanFactory {
<<interface>>
+createBean(Class) Object
+autowireBean(Object)
}
class ConfigurableListableBeanFactory {
<<interface>>
+preInstantiateSingletons()
+getBeanDefinition(String) BeanDefinition
}
class BeanDefinitionRegistry {
<<interface>>
+registerBeanDefinition(String, BeanDefinition)
+removeBeanDefinition(String)
}
BeanFactory <|-- ListableBeanFactory
BeanFactory <|-- HierarchicalBeanFactory
HierarchicalBeanFactory <|-- ConfigurableBeanFactory
ConfigurableBeanFactory <|-- ConfigurableListableBeanFactory
ListableBeanFactory <|-- ConfigurableListableBeanFactory
BeanFactory <|-- AutowireCapableBeanFactory
ConfigurableListableBeanFactory <|.. DefaultListableBeanFactory
BeanDefinitionRegistry <|.. DefaultListableBeanFactory
Why so many interfaces? Three reasons:
-
Read vs Write segregation.
BeanFactoryandListableBeanFactoryare read-only.ConfigurableBeanFactoryadds write operations. Application code gets the read interfaces; infrastructure code gets the configurable ones. -
Listable vs Hierarchical.
ListableBeanFactorycan enumerate all beans.HierarchicalBeanFactorysupports parent-child factory chains (used in web applications where each servlet gets its own child context). These are orthogonal concerns. -
SPI vs Client API.
BeanDefinitionRegistryis a pure SPI for registering bean recipes. It's implemented byDefaultListableBeanFactorybut intentionally not part of theBeanFactoryclient hierarchy.
The Template Method Chain: From Registry to Factory
The implementation hierarchy mirrors the interface pyramid with four key classes, each adding a specific layer of responsibility:
spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java#L118
classDiagram
class DefaultSingletonBeanRegistry {
-singletonObjects: Map
-singletonFactories: Map
-earlySingletonObjects: Map
+getSingleton(String) Object
#addSingleton(String, Object)
}
class AbstractBeanFactory {
#getBean(String) Object
#doGetBean(String) Object
#createBean(String, RootBeanDefinition)*
}
class AbstractAutowireCapableBeanFactory {
#createBean(String, RootBeanDefinition) Object
#doCreateBean(String, RootBeanDefinition) Object
#populateBean(String, RootBeanDefinition, BeanWrapper)
#initializeBean(String, Object, RootBeanDefinition) Object
}
class DefaultListableBeanFactory {
-beanDefinitionMap: Map
+preInstantiateSingletons()
+registerBeanDefinition(String, BeanDefinition)
}
DefaultSingletonBeanRegistry <|-- AbstractBeanFactory
AbstractBeanFactory <|-- AbstractAutowireCapableBeanFactory
AbstractAutowireCapableBeanFactory <|-- DefaultListableBeanFactory
Each layer has a clear job:
- DefaultSingletonBeanRegistry: Manages the singleton cache (the three-level cache for circular references).
- AbstractBeanFactory: Implements
getBean()and the template methodcreateBean(). Handles merged bean definitions, FactoryBean dereferencing, and scope resolution. - AbstractAutowireCapableBeanFactory: Implements
createBean()— the actual bean creation pipeline including constructor resolution, property injection, and initialization. - DefaultListableBeanFactory: Adds bean definition storage, enumeration (
getBeansOfType()), and pre-instantiation of singletons.
The Bean Creation Pipeline: createBean() to Ready-to-Use
When you call getBean("myService"), the container eventually reaches AbstractAutowireCapableBeanFactory.createBean(). This method orchestrates a five-phase pipeline:
sequenceDiagram
participant Client
participant ABF as AbstractBeanFactory
participant AACBF as AbstractAutowireCapableBeanFactory
participant BPP as BeanPostProcessors
Client->>ABF: getBean("myService")
ABF->>AACBF: createBean(name, mbd, args)
Note over AACBF: Phase 0: resolveBeforeInstantiation
AACBF->>BPP: postProcessBeforeInstantiation()
Note right of BPP: Can short-circuit with proxy
AACBF->>AACBF: doCreateBean(name, mbd, args)
Note over AACBF: Phase 1: createBeanInstance
AACBF->>AACBF: Constructor resolution + instantiation
Note over AACBF: Phase 2: applyMergedBeanDefinitionPostProcessors
AACBF->>BPP: postProcessMergedBeanDefinition()
Note right of BPP: @Autowired metadata caching
Note over AACBF: Phase 3: Early singleton exposure
AACBF->>AACBF: addSingletonFactory() for circular refs
Note over AACBF: Phase 4: populateBean
AACBF->>BPP: postProcessProperties()
Note right of BPP: Actually injects @Autowired fields
Note over AACBF: Phase 5: initializeBean
AACBF->>AACBF: invokeAwareMethods()
AACBF->>BPP: postProcessBeforeInitialization()
AACBF->>AACBF: invokeInitMethods() [afterPropertiesSet]
AACBF->>BPP: postProcessAfterInitialization()
Note right of BPP: AOP proxies created here
AACBF-->>Client: Fully initialized bean
Let's look at doCreateBean() where the core phases execute:
Phase 1 — createBeanInstance(): Resolves the constructor (or factory method) and instantiates the raw object. At this point, the bean has no injected dependencies.
Phase 2 — applyMergedBeanDefinitionPostProcessors(): MergedBeanDefinitionPostProcessor implementations scan the bean class for injection metadata. This is where AutowiredAnnotationBeanPostProcessor discovers @Autowired fields and methods, caching the metadata for Phase 4.
Phase 3 — Early singleton exposure: If circular references are allowed, the not-yet-fully-initialized bean is exposed via a singletonFactory lambda at line 596. This is what breaks circular dependencies.
Phase 4 — populateBean(): Actually performs dependency injection. InstantiationAwareBeanPostProcessor.postProcessProperties() is called, which triggers AutowiredAnnotationBeanPostProcessor to inject the dependencies it discovered in Phase 2.
Phase 5 — initializeBean(): The final initialization sequence deserves its own closer look.
The initializeBean Ceremony
The initializeBean() method executes a precise ceremony:
invokeAwareMethods()— CallssetBeanName(),setBeanClassLoader(),setBeanFactory()on beans implementing the respectiveAwareinterfaces.applyBeanPostProcessorsBeforeInitialization()— Every registeredBeanPostProcessorgets a chance to inspect or wrap the bean before initialization callbacks.invokeInitMethods()— CallsInitializingBean.afterPropertiesSet()and then any custom init-method declared in the bean definition.applyBeanPostProcessorsAfterInitialization()— The crucial hook where AOP proxies are created. This is where@Transactional,@Async, and other proxy-based features wrap the original bean.
The BeanFactory Javadoc at lines 69–98 documents this entire lifecycle in order — it's one of the most important Javadoc blocks in the framework:
spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java#L69-L98
Circular Reference Resolution: The Three-Level Cache
One of Spring's most complex and under-documented features is its ability to resolve circular dependencies between singleton beans. Bean A depends on Bean B, and Bean B depends on Bean A. Without special handling, this would be an infinite loop.
Spring solves this with three maps in DefaultSingletonBeanRegistry:
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Creation-time registry of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16);
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
stateDiagram-v2
[*] --> Creating: getBean("A") called
Creating --> InSingletonFactories: createBeanInstance() done,<br/>addSingletonFactory() called
InSingletonFactories --> InEarlySingletonObjects: Another bean requests "A",<br/>factory.getObject() called
InEarlySingletonObjects --> InSingletonObjects: initializeBean() completes,<br/>addSingleton() called
InSingletonObjects --> [*]: Fully ready
note right of InSingletonFactories
Level 3: ObjectFactory lambda
that may create AOP proxy
end note
note right of InEarlySingletonObjects
Level 2: Raw or proxied reference
before full initialization
end note
note right of InSingletonObjects
Level 1: Fully initialized singleton
end note
The resolution algorithm in getSingleton() walks the cache levels:
- Check
singletonObjects(level 1) — the bean is fully ready. - Check
earlySingletonObjects(level 2) — already promoted from factory. - Check
singletonFactories(level 3) — invoke the factory, promote to level 2, and return the early reference.
The factory at level 3 is the lambda () -> getEarlyBeanReference(beanName, mbd, bean) registered during Phase 3 of doCreateBean(). When AOP is involved, getEarlyBeanReference() can return a proxy rather than the raw bean — this is why the factory exists rather than caching the raw instance directly.
Tip: Circular dependencies only work for singleton-scoped beans using setter/field injection. Constructor injection circular dependencies will always throw
BeanCurrentlyInCreationExceptionbecause the raw object doesn't exist yet when the constructor needs its dependencies.
BeanPostProcessor: The Universal Extension Hook
spring-beans/src/main/java/org/springframework/beans/factory/config/BeanPostProcessor.java#L67-L111
BeanPostProcessor is arguably the single most important interface in the framework. With just two default methods — postProcessBeforeInitialization() and postProcessAfterInitialization() — it enables virtually every Spring feature:
flowchart TD
BPP[BeanPostProcessor]
BPP --> AABPP["AutowiredAnnotationBeanPostProcessor<br/>@Autowired, @Value injection"]
BPP --> CABPP["CommonAnnotationBeanPostProcessor<br/>@PostConstruct, @Resource"]
BPP --> AAPC["AbstractAutoProxyCreator<br/>AOP proxies for @Transactional, @Async, @Cacheable"]
BPP --> ALP["ApplicationListenerDetector<br/>Registers @EventListener methods"]
BPP --> SCBPP["ScheduledAnnotationBeanPostProcessor<br/>@Scheduled methods"]
BPP --> Custom["Your custom BeanPostProcessor"]
The interface has a sub-interface hierarchy too: InstantiationAwareBeanPostProcessor adds hooks before instantiation (used by AOP), and SmartInstantiationAwareBeanPostProcessor adds predictive type resolution and early proxy creation for circular reference handling.
The ordering mechanism — PriorityOrdered and Ordered interfaces — is critical. Infrastructure processors like AutowiredAnnotationBeanPostProcessor implement PriorityOrdered to run first, before any user-level processors.
What's Next
We've now mapped the entire IoC container: from the interface pyramid that separates read/write/list concerns, through the template method chain that builds beans layer by layer, to the three-level cache that resolves circular dependencies, and finally to BeanPostProcessor — the universal hook that makes Spring extensible. In the next article, we'll zoom out from individual bean creation to the application startup sequence: AbstractApplicationContext.refresh(), the 13-step method that brings an entire Spring application to life.