🍃 Section 5 · Question #6

What is Bean Lifecycle

The Bean Lifecycle is the deterministic sequence of phases that an object traverses under the management of the Spring Inversion of Control (IoC) Container: from reading configu...


🟢 Junior Level

1. What is the Bean Lifecycle?

The Bean Lifecycle is the deterministic sequence of phases that an object traverses under the management of the Spring Inversion of Control (IoC) Container: from reading configuration metadata (BeanDefinition), allocating memory in the JVM heap, and injecting collaborators, to executing initialization callbacks, servicing runtime requests, and gracefully releasing resources upon container shutdown.

Bean Lifecycle Progression:
1. Instantiation         ──► Allocates raw object in heap via constructor
         │
         ▼
2. Populate Properties   ──► Injects dependencies (@Autowired fields and setters)
         │
         ▼
3. Initialization        ──► Executes @PostConstruct, afterPropertiesSet, init-method
         │
         ▼
4. In Service / Ready    ──► Fully assembled bean services application traffic
         │
         ▼
5. Destruction           ──► Executes @PreDestroy, DisposableBean, destroy-method

[!IMPORTANT] Jakarta EE Migration (Spring Boot 3+ / Spring Framework 6+): Lifecycle annotations migrated from javax.annotation.* to jakarta.annotation.*:

  • @PostConstruct $\to$ jakarta.annotation.PostConstruct
  • @PreDestroy $\to$ jakarta.annotation.PreDestroy

2. Basic Lifecycle Example in Spring Boot 3

package com.example.service;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;

@Service
public class PaymentProcessingService {

    private final PaymentGatewayClient gatewayClient;

    // 1. Instantiation: Container invokes constructor and injects dependency
    public PaymentProcessingService(PaymentGatewayClient gatewayClient) {
        this.gatewayClient = gatewayClient;
        System.out.println("1. Instantiation: Object created in heap");
    }

    // 3. Initialization: Called AFTER all dependencies are fully wired
    @PostConstruct
    public void init() {
        System.out.println("3. Initialization: Pre-warming connections & validating credentials");
        gatewayClient.ping();
    }

    // 4. In Service: Business logic
    public void processPayment(double amount) {
        gatewayClient.charge(amount);
    }

    // 5. Destruction: Called during ApplicationContext shutdown
    @PreDestroy
    public void cleanup() {
        System.out.println("5. Destruction: Closing network sockets & releasing buffers");
        gatewayClient.disconnect();
    }
}

🟡 Middle Level

1. Detailed Lifecycle Execution Pipeline

Inside the Spring container (AbstractAutowireCapableBeanFactory), the complete lifecycle unfolds across strict, fine-grained phases:

[ BeanDefinition Parsing ] (Reads XML, @Component, @Bean metadata)
            │
            ▼
[ BeanFactoryPostProcessor ] (Modifies metadata BEFORE any bean is created)
            │
            ▼
[ Instantiation ] (Constructor chosen; raw instance created in heap)
            │
            ▼
[ Populate Properties ] (AutowiredAnnotationBeanPostProcessor injects fields & setters)
            │
            ▼
[ Aware Interfaces ] (BeanNameAware, BeanFactoryAware, ApplicationContextAware)
            │
            ▼
[ BPP: postProcessBeforeInitialization ] (@PostConstruct via CommonAnnotationBeanPostProcessor)
            │
            ▼
[ InitializingBean: afterPropertiesSet() ]
            │
            ▼
[ Custom init-method ] (@Bean(initMethod = "customInit"))
            │
            ▼
[ BPP: postProcessAfterInitialization ] (Wraps bean in AOP Dynamic Proxy / CGLIB)
            │
            ▼
[ IN SERVICE / READY TO USE ]
            │
            ▼
[ Destruction Phase ] (@PreDestroy ──► DisposableBean.destroy() ──► custom destroy-method)

2. Aware Interfaces

If a bean requires access to internal Spring infrastructure, it implements marker Aware interfaces. Spring injects these infrastructure references in a strict sequence right before initialization:

  • BeanNameAware: Supplies the bean’s registered string ID in the container.
  • BeanClassLoaderAware: Supplies the ClassLoader used by the container.
  • BeanFactoryAware: Supplies the low-level BeanFactory instance.
  • EnvironmentAware: Supplies active profiles and environment properties.
  • ApplicationContextAware: Supplies the high-level ApplicationContext (injected via ApplicationContextAwareProcessor).

3. Singleton vs Prototype Scope Lifecycle Invariant

The lifecycle behavior of Spring beans differs drastically depending on their declared scope:

Lifecycle Stage singleton Scope prototype Scope
Creation Once at context startup (unless @Lazy) On every getBean() call or injection
Dependency Injection Wired once Wired every time a new instance is created
@PostConstruct Executed once by container Executed for every new instance
Container Reference Retained permanently in singleton cache Discarded immediately after initialization!
@PreDestroy Hook Executed on context shutdown ❌ NEVER EXECUTED BY SPRING!

[!CAUTION] Prototype Memory & Resource Leak Hazard: Spring completely forgets about a prototype bean immediately after handing it to the caller. The container does not track prototype destruction and will never execute @PreDestroy or DisposableBean.destroy()!

If a prototype bean acquires file handles, database connections, or background threads, the client code must explicitly invoke destruction methods:

PrototypeWorker worker = context.getBean(PrototypeWorker.class);
try {
    worker.doWork();
} finally {
    worker.cleanup(); // Must be manually cleaned up by client!
}

4. Global Subsystem Coordination: SmartLifecycle

When an application needs to start or stop entire infrastructure subsystems (e.g., Kafka listeners, Netty servers, WebSocket endpoints) in a coordinated order after all beans and proxies are fully assembled, it implements SmartLifecycle:

@Component
public class KafkaConsumerCoordinator implements SmartLifecycle {

    private volatile boolean running = false;

    @Override
    public void start() {
        this.running = true;
        System.out.println("SmartLifecycle: Kafka listener started AFTER all proxies initialized");
    }

    @Override
    public void stop() {
        this.running = false;
        System.out.println("SmartLifecycle: Kafka listener gracefully stopped");
    }

    @Override
    public boolean isRunning() {
        return running;
    }

    @Override
    public int getPhase() {
        return 100; // Lower numbers start earlier and stop later
    }

    @Override
    public boolean isAutoStartup() {
        return true;
    }
}

🔴 Senior Level

1. Internal Engine: AbstractAutowireCapableBeanFactory.doCreateBean

In OpenJDK Spring Framework, the core bean manufacturing pipeline is implemented inside AbstractAutowireCapableBeanFactory.doCreateBean():

// Conceptual flow inside doCreateBean():
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
    // 1. Instantiation:
    BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
    Object bean = instanceWrapper.getWrappedInstance();

    // 2. Early Singleton Exposure (Resolving circular references via 3-level cache):
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
            isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    // 3. Property Population:
    populateBean(beanName, mbd, instanceWrapper);

    // 4. Initialization & AOP Proxy Generation:
    Object exposedObject = initializeBean(beanName, bean, mbd);

    return exposedObject;
}

Detailed Breakdown:

  1. createBeanInstance: Resolves constructors and instantiates the raw object via Constructor.newInstance(). The object exists in heap, but non-constructor fields are null.
  2. addSingletonFactory: Registers an ObjectFactory lambda in the 3rd level cache (singletonFactories) so that circular collaborators can obtain an early reference.
  3. populateBean: Calls InstantiationAwareBeanPostProcessor.postProcessProperties() to inject fields and setters via reflection.
  4. initializeBean:
    • Calls Aware interfaces (BeanNameAware, BeanFactoryAware).
    • Executes postProcessBeforeInitialization (@PostConstruct).
    • Calls InitializingBean.afterPropertiesSet() and custom init-method.
    • Executes postProcessAfterInitialization: AOP Dynamic Proxies (JDK/CGLIB) are generated here by AbstractAutoProxyCreator.

2. The 3-Level Singleton Cache Architecture

To resolve circular dependencies without deadlocks during field/setter injection, DefaultSingletonBeanRegistry uses three synchronized maps:

DefaultSingletonBeanRegistry Internal Tiers:
├── Level 1: singletonObjects (ConcurrentHashMap<String, Object>)
│   └── Ready-to-use, fully initialized, and proxied singleton beans.
│
├── Level 2: earlySingletonObjects (HashMap<String, Object>)
│   └── Unpopulated raw or early-proxied references exposed to break cycles.
│
└── Level 3: singletonFactories (HashMap<String, ObjectFactory<?>>)
    └── Factories generating early references/proxies only when circularity is detected.

The Lookup Sequence in getSingleton(beanName, allowEarlyReference):

  1. Check singletonObjects (Level 1). Found? Return it.
  2. Not found and bean is currently in creation? Check earlySingletonObjects (Level 2). Found? Return it.
  3. Not found? Synchronize on singletonObjects lock. Check Level 2 again.
  4. Retrieve ObjectFactory from singletonFactories (Level 3), invoke factory.getObject(), store in earlySingletonObjects, and remove from singletonFactories.

[!NOTE] Why Constructor Injection Cannot Use the 3-Level Cache: To register an entry in singletonFactories, the object must first be allocated in the heap. In Constructor Injection, Thread A blocks inside createBeanInstance(A) waiting for parameter $B$, while Thread B blocks inside createBeanInstance(B) waiting for parameter $A$. Neither object can be allocated, triggering an immediate BeanCurrentlyInCreationException.


3. The Fatal @PostConstruct AOP Proxy Trap

A very common architectural bug is placing @Transactional or @Async on a method called inside @PostConstruct:

@Service
public class CurrencyService {

    @PostConstruct
    public void init() {
        // ❌ CRITICAL BUG: Transaction will NOT open!
        loadInitialRates(); 
    }

    @Transactional
    public void loadInitialRates() {
        // Database write operations executed WITHOUT an active transaction!
    }
}

Why This Fails:

Lifecycle Sequence:
1. Object Instantiation (Raw instance created)
2. Dependency Population (Fields wired)
3. @PostConstruct executed ──► Operates on raw `this` (Target Object)!
4. postProcessAfterInitialization ──► AOP Dynamic Proxy wraps Target Object!

At the moment @PostConstruct executes, the AOP Dynamic Proxy does not exist yet! The call executes directly on the raw target instance, bypassing the TransactionInterceptor.

The Production-Grade Solution:

Use an application event listener that fires after the container has fully initialized all beans and proxies:

@Service
public class CurrencyService {

    @EventListener(ApplicationReadyEvent.class) // ✅ Fires AFTER all proxies are active!
    @Transactional
    public void onApplicationReady() {
        loadInitialRates(); // Runs inside a fully active database transaction!
    }

    public void loadInitialRates() { ... }
}

4. Java Memory Model (JMM) Safe Publication Guarantees

Spring guarantees thread safety when multiple worker threads read singleton beans:

  1. Constructor Injection: Fields declared final benefit from JMM §17.5 freeze action guarantees, ensuring immediate visibility across CPU cores without memory barriers.
  2. Setter / Field Injection: Although fields are non-final, Spring publishes the completed singleton into singletonObjects inside a synchronized (this.singletonObjects) block, and worker threads read from ConcurrentHashMap. This establishes a formal happens-before edge under the JMM, ensuring safe publication.

4 Tricky Questions

1. Why do @Transactional and @Async fail to function when called from inside a @PostConstruct method?

Answer: AOP dynamic proxies (which intercept method invocations to manage transactions or asynchronous thread dispatch) are created during the very last step of initialization: inside BeanPostProcessor.postProcessAfterInitialization().

The @PostConstruct hook runs earlier in the lifecycle, inside postProcessBeforeInitialization(). When @PostConstruct runs, the bean is still a raw, unproxied Java instance (this). Invoking another method on this executes direct bytecode calls on the unproxied instance, completely bypassing Spring’s TransactionInterceptor and AsyncExecutionInterceptor.

Remedy: Place startup transactional logic inside an event listener listening for ApplicationReadyEvent or ContextRefreshedEvent.


2. What happens to a @Scope("prototype") bean when the ApplicationContext is closed, and why?

Answer: Nothing happens. Spring does not call @PreDestroy or DisposableBean.destroy() on prototype-scoped beans.

Spring’s design contract states that the container creates, configures, wires, and initializes prototype beans, and then immediately surrenders ownership to the calling client. If Spring retained references to all prototype beans to track their destruction, the container would quickly suffer catastrophic memory leaks. Therefore, managing the destruction and resource cleanup of prototype beans is strictly the responsibility of the client code that requested them.


3. Why does the 3-level singleton cache resolve circular dependencies for field injection, but fails fast for constructor injection?

Answer: For field injection, Spring first calls the default constructor to allocate a raw, empty instance in heap memory. It immediately wraps this raw instance in an ObjectFactory and places it into the 3rd level cache (singletonFactories). When collaborator Bean $B$ is created, it retrieves this early reference, completes its own lifecycle, and allows Bean $A$ to finish.

With Constructor Injection, an object cannot be instantiated in heap memory without its constructor parameters being satisfied first. Spring gets blocked during createBeanInstance(A) because parameter $B$ is missing, and blocked during createBeanInstance(B) because parameter $A$ is missing. Because neither instance can be allocated in heap, no early reference can be published to singletonFactories, causing Spring to throw a fail-fast BeanCurrentlyInCreationException.


4. What is the fundamental difference between BeanFactoryPostProcessor (BFPP) and BeanPostProcessor (BPP)?

Answer:

  • BeanFactoryPostProcessor (BFPP) operates on BeanDefinition metadata before any bean instances are created in heap memory. It can modify bean scopes, change property values, or resolve placeholders (e.g., PropertySourcesPlaceholderConfigurer resolving ${db.url}).
  • BeanPostProcessor (BPP) operates on actual physical Java bean instances in heap memory after they have been instantiated. Its methods (postProcessBeforeInitialization and postProcessAfterInitialization) run for every bean, handling @Autowired injection, @PostConstruct execution, and wrapping beans into AOP dynamic proxies.

🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“The Spring Bean Lifecycle represents the complete progression of a managed bean across five macro-phases:

  1. BeanDefinition Parsing & BFPP (Metadata configuration).
  2. Instantiation (Raw object created via constructor).
  3. Property Population (Fields and setters injected via BPP).
  4. Initialization (Aware callbacks $\to$ @PostConstruct $\to$ InitializingBean $\to$ custom init-method $\to$ AOP Dynamic Proxy creation).
  5. Destruction (@PreDestroy $\to$ DisposableBean $\to$ custom destroy-method).

Proxies are created after @PostConstruct, which is why @Transactional does not work inside @PostConstruct. For prototype beans, Spring never calls destruction callbacks.”


Red Flags & Pitfalls (DO NOT Say)

  • ❌ “In the constructor, @Autowired fields are already populated.” (Fields are null inside the constructor unless passed via constructor injection).
  • ❌ “Spring calls @PreDestroy for prototype beans when they are garbage collected.” (Spring never calls destruction hooks on prototype beans).
  • ❌ “You can execute transactional database updates inside @PostConstruct.” (AOP proxies are created after @PostConstruct; transactions will not open).
  • ❌ “Instantiation and initialization are the same phase.” (Instantiation creates the raw instance in heap; initialization runs after all dependencies are injected).

Complete Phase Progression Checklist

Spring Bean Lifecycle Progression Table:
┌───────┬──────────────────────────────────────────┬────────────────────────────────────────┐
│ Phase │ Hook / Operation                         │ Internal Actor / Mechanism             │
├───────┼──────────────────────────────────────────┼────────────────────────────────────────┤
│ Meta  │ BeanDefinition Metadata Parsing          │ ClassPathBeanDefinitionScanner (ASM)   │
│ Meta  │ BeanFactoryPostProcessor                 │ PropertySourcesPlaceholderConfigurer   │
│ 1     │ Constructor Selection & Instantiation    │ Constructor.newInstance() / CGLIB      │
│ 2     │ Early Singleton Factory Exposure         │ DefaultSingletonBeanRegistry (Level 3) │
│ 3     │ Property Population (Field/Setter DI)    │ AutowiredAnnotationBeanPostProcessor   │
│ 4     │ Aware Interfaces Injected                │ BeanNameAware, ApplicationContextAware │
│ 5     │ @PostConstruct Callback                  │ CommonAnnotationBeanPostProcessor      │
│ 6     │ InitializingBean.afterPropertiesSet()    │ Direct interface invocation            │
│ 7     │ Custom init-method                       │ Configured via @Bean(initMethod = "…") │
│ 8     │ AOP Proxy Generation                     │ AbstractAutoProxyCreator (BPP After)   │
│ READY │ Bean In Service                          │ Servicing application traffic          │
│ 9     │ @PreDestroy Callback                     │ InitDestroyAnnotationBeanPostProcessor │
│ 10    │ DisposableBean.destroy()                 │ Direct interface invocation            │
│ 11    │ Custom destroy-method                    │ Configured via @Bean(destroyMethod)    │
└───────┴──────────────────────────────────────────┴────────────────────────────────────────┘