Read OSS

The IoC Container Internals: BeanFactory Hierarchy and Bean Creation Pipeline

Advanced

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:

  1. Read vs Write segregation. BeanFactory and ListableBeanFactory are read-only. ConfigurableBeanFactory adds write operations. Application code gets the read interfaces; infrastructure code gets the configurable ones.

  2. Listable vs Hierarchical. ListableBeanFactory can enumerate all beans. HierarchicalBeanFactory supports parent-child factory chains (used in web applications where each servlet gets its own child context). These are orthogonal concerns.

  3. SPI vs Client API. BeanDefinitionRegistry is a pure SPI for registering bean recipes. It's implemented by DefaultListableBeanFactory but intentionally not part of the BeanFactory client 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/DefaultSingletonBeanRegistry.java#L76

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java#L118

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java#L121

spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java#L132-L133

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 method createBean(). 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:

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java#L488-L540

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:

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java#L556-L607

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

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java#L1799-L1824

The initializeBean() method executes a precise ceremony:

  1. invokeAwareMethods() — Calls setBeanName(), setBeanClassLoader(), setBeanFactory() on beans implementing the respective Aware interfaces.
  2. applyBeanPostProcessorsBeforeInitialization() — Every registered BeanPostProcessor gets a chance to inspect or wrap the bean before initialization callbacks.
  3. invokeInitMethods() — Calls InitializingBean.afterPropertiesSet() and then any custom init-method declared in the bean definition.
  4. 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:

spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java#L86-L95

/** 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:

spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java#L208-L237

  1. Check singletonObjects (level 1) — the bean is fully ready.
  2. Check earlySingletonObjects (level 2) — already promoted from factory.
  3. 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 BeanCurrentlyInCreationException because 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.