🍃 Section 5 · Question #7

What are the Bean Lifecycle stages

The Spring Bean lifecycle describes the deterministic progression an object traverses from container discovery to destruction:


🟢 Junior Level

1. The 5 Simplified Macro-Phases

The Spring Bean lifecycle describes the deterministic progression an object traverses from container discovery to destruction:

1. Instantiation (Constructor)          ──► Allocates raw instance in heap
              │
              ▼
2. Populate Properties (@Autowired)     ──► Injects dependencies into fields/setters
              │
              ▼
3. Initialization (@PostConstruct)      ──► Validates configuration & pre-warms caches
              │
              ▼
4. Ready for Use (In Service)           ──► Actively services business logic
              │
              ▼
5. Destruction (@PreDestroy)            ──► Flushes buffers & closes network sockets

2. Concrete Code Demonstration of Execution Order

package com.example.demo;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class NotificationManager {

    private TemplateRepository templateRepository;

    // Phase 1: Constructor (Instantiation)
    public NotificationManager() {
        System.out.println("1. Constructor called. templateRepository is null: " 
                + (templateRepository == null));
    }

    // Phase 2: Dependency Injection (Populate Properties)
    @Autowired
    public void setTemplateRepository(TemplateRepository templateRepository) {
        this.templateRepository = templateRepository;
        System.out.println("2. Dependencies wired via setter injection");
    }

    // Phase 3: Initialization
    @PostConstruct
    public void init() {
        System.out.println("3. @PostConstruct called: dependencies are wired, pre-loading templates");
        templateRepository.preload();
    }

    // Phase 4: Business Method (Ready for Use)
    public void sendNotification(String message) {
        System.out.println("4. Sending notification: " + message);
    }

    // Phase 5: Destruction
    @PreDestroy
    public void cleanup() {
        System.out.println("5. @PreDestroy called: releasing connection pools");
    }
}

🟡 Middle Level

1. The Canonical 12-Stage Lifecycle Sequence

Under the hood, Spring’s AbstractAutowireCapableBeanFactory executes a strict 12-stage pipeline:

[ 1. Instantiation ]
        Selects constructor and allocates raw Java object in heap.
             │
             ▼
[ 2. Populate Properties ]
        Injects dependencies into fields and setters via postProcessProperties().
             │
             ▼
[ 3. Aware Interfaces Callbacks ]
        Injects BeanNameAware, BeanClassLoaderAware, BeanFactoryAware.
             │
             ▼
[ 4. BPP: postProcessBeforeInitialization ]
        CommonAnnotationBeanPostProcessor executes @PostConstruct methods.
        ApplicationContextAwareProcessor injects ApplicationContext.
             │
             ▼
[ 5. InitializingBean.afterPropertiesSet() ]
        Spring Framework contractual callback interface.
             │
             ▼
[ 6. Custom init-method ]
        Method configured via @Bean(initMethod = "customInit").
             │
             ▼
[ 7. BPP: postProcessAfterInitialization ]
        ⭐ AOP Dynamic Proxies (CGLIB / JDK Dynamic Proxy) generated here!
             │
             ▼
[ 8. BEAN READY FOR USE ]
        Fully assembled bean published into singletonObjects cache.
             │
             ▼
[ 9. Context Shutdown Initiated ]
        Graceful shutdown hook triggered (e.g. JVM SIGTERM).
             │
             ▼
[ 10. @PreDestroy ]
        Executed by InitDestroyAnnotationBeanPostProcessor.
             │
             ▼
[ 11. DisposableBean.destroy() ]
        Spring Framework contractual callback interface.
             │
             ▼
[ 12. Custom destroy-method ]
        Method configured via @Bean(destroyMethod = "customCleanup").

2. Comparison of the 3 Initialization & Destruction Mechanisms

Spring provides three distinct ways to hook into initialization and destruction, invoked strictly in the following sequence:

Order Initialization Mechanism Destruction Mechanism Standard / Specification Production Recommendation
1st @PostConstruct @PreDestroy JSR-250 / Jakarta EE Annotations Recommended (95%) (Clean, framework-independent POJO code)
2nd InitializingBean.afterPropertiesSet() DisposableBean.destroy() Spring Framework Interface Legacy / Framework SPI (Avoid: couples code to Spring imports)
3rd @Bean(initMethod = "…") @Bean(destroyMethod = "…") Declarative Java Configuration Ideal for 3rd-party code without source annotations
public class LifecycleDemoBean implements InitializingBean, DisposableBean {

    // 1st Init Callback
    @PostConstruct
    public void postConstruct() {
        System.out.println("1st Init: @PostConstruct (Jakarta Standard)");
    }

    // 2nd Init Callback
    @Override
    public void afterPropertiesSet() {
        System.out.println("2nd Init: InitializingBean.afterPropertiesSet()");
    }

    // 3rd Init Callback (declared in @Bean(initMethod = "customInit"))
    public void customInit() {
        System.out.println("3rd Init: Custom init-method");
    }

    // 1st Destruction Callback
    @PreDestroy
    public void preDestroy() {
        System.out.println("1st Destroy: @PreDestroy (Jakarta Standard)");
    }

    // 2nd Destruction Callback
    @Override
    public void destroy() {
        System.out.println("2nd Destroy: DisposableBean.destroy()");
    }

    // 3rd Destruction Callback (declared in @Bean(destroyMethod = "customDestroy"))
    public void customDestroy() {
        System.out.println("3rd Destroy: Custom destroy-method");
    }
}

🔴 Senior Level

1. Source Code Walkthrough: AbstractAutowireCapableBeanFactory

In OpenJDK Spring Framework, the core lifecycle orchestrator is implemented in doCreateBean and initializeBean:

// AbstractAutowireCapableBeanFactory.java (Conceptualized snippet):
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
    // Stage 1: Instantiation
    BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
    Object bean = instanceWrapper.getWrappedInstance();

    // Early Singleton Exposure for Circular Dependencies:
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences 
            && isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    // Stage 2: Populate Properties (Field and Setter Injection)
    populateBean(beanName, mbd, instanceWrapper);

    // Stages 3-7: Initialize Bean
    Object exposedObject = initializeBean(beanName, bean, mbd);

    // Register destruction callbacks for singletons
    registerDisposableBeanIfNecessary(beanName, bean, mbd);

    return exposedObject;
}

protected Object initializeBean(String beanName, Object bean, RootBeanDefinition mbd) {
    // Stage 3: Aware interfaces
    invokeAwareMethods(beanName, bean);

    // Stage 4: BPP Before Initialization (@PostConstruct)
    Object wrappedBean = applyBeanPostProcessorsBeforeInitialization(bean, beanName);

    // Stages 5-6: InitializingBean & custom initMethod
    invokeInitMethods(beanName, wrappedBean, mbd);

    // Stage 7: BPP After Initialization (AOP Proxy Wrapping!)
    wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);

    return wrappedBean;
}

2. The Premature BPP Initialization Hazard (“Naked Bean” Defect)

A notoriously obscure defect in enterprise Spring applications triggers the warning:

Bean 'myService' of type [com.example.MyService] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

The Architectural Root Cause:

  1. BeanPostProcessor (BPP) beans are system-level interceptors that must be instantiated before regular application beans so they can intercept user beans during initialization.
  2. If a custom BPP declares a direct dependency on an application bean (e.g. AuditService or UserRepository):
    @Component
    public class CustomSecurityBpp implements BeanPostProcessor {
        @Autowired
        private AuditService auditService; // ❌ SEVERE ARCHITECTURAL FLAW!
    }
    
  3. To instantiate CustomSecurityBpp, Spring must immediately instantiate AuditService prematurely, before other infrastructure BPPs (such as AnnotationAwareAspectJAutoProxyCreator) are even registered!
  4. Catastrophic Result: AuditService is created as a “naked” instance. It never gets wrapped by the AOP proxy. Any annotations like @Transactional, @Async, or @Cacheable on AuditService silently fail to function!

Production Solution:

Break the premature dependency using ObjectProvider<T> or @Lazy:

@Component
public class CustomSecurityBpp implements BeanPostProcessor {
    private final ObjectProvider<AuditService> auditServiceProvider;

    public CustomSecurityBpp(ObjectProvider<AuditService> auditServiceProvider) {
        this.auditServiceProvider = auditServiceProvider;
    }

    private void logSecurityEvent() {
        // Resolve on demand when processing user beans, not during BPP startup!
        auditServiceProvider.ifAvailable(AuditService::log);
    }
}

3. AOP Proxy Timing: Normal vs Circular Dependency

  • Standard Execution Flow: The AOP Dynamic Proxy is created at Stage 7 (BPP.postProcessAfterInitialization) via AbstractAutoProxyCreator.
  • Circular Dependency Flow ($A \to B \to A$):
    1. Bean $A$ instantiates and publishes its lambda factory to Level 3 cache (singletonFactories).
    2. Bean $A$ attempts to populate dependencies and triggers instantiation of Bean $B$.
    3. Bean $B$ requires Bean $A$. It requests Bean $A$ from the 3-level cache.
    4. The Level 3 factory invokes SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference(A). The AOP Proxy for Bean $A$ is created prematurely right here and cached in Level 2 (earlySingletonObjects).
    5. When Bean $A$ later reaches Stage 7, AbstractAutoProxyCreator recognizes that an early proxy was already generated and returns the existing proxy without re-wrapping.

4 Tricky Questions

1. In what exact order do @PostConstruct, afterPropertiesSet(), and custom initMethod execute, and why?

Answer: They execute in the following strict order:

  1. @PostConstruct: Triggered first during Stage 4 by CommonAnnotationBeanPostProcessor.postProcessBeforeInitialization().
  2. InitializingBean.afterPropertiesSet(): Triggered second during Stage 5 via direct invocation of the interface contract by invokeInitMethods().
  3. Custom initMethod (from @Bean(initMethod = "...")): Triggered third during Stage 6 by reflection in invokeInitMethods().

@PostConstruct is favored in modern applications because it is a Jakarta standard annotation (jakarta.annotation.PostConstruct) that avoids tying your domain code to Spring Framework interface APIs.


2. What causes the warning “Bean is not eligible for getting processed by all BeanPostProcessors”, and what is the severe consequence?

Answer: This warning occurs when a regular application bean is pulled into the container’s startup sequence prematurely as a dependency of a BeanPostProcessor.

Because this bean is instantiated before the full set of BeanPostProcessor instances (specifically AOP auto-proxy creators like AnnotationAwareAspectJAutoProxyCreator) have been registered, it skips post-processing. The bean remains a “naked” instance and will never be wrapped in an AOP dynamic proxy. As a result, cross-cutting features like @Transactional, @Async, @Cacheable, and security checks on that bean silently stop working.


3. At which exact lifecycle stage is an AOP Dynamic Proxy generated in standard vs circular dependency flows?

Answer:

  • Standard Flow: The AOP dynamic proxy is created at Stage 7 (BeanPostProcessor.postProcessAfterInitialization).
  • Circular Dependency Flow: The proxy is generated prematurely during Stage 2 (Property Population) when a collaborator bean requests an early reference. The Level 3 singletonFactories cache invokes SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference(), which constructs the proxy and stores it in Level 2 (earlySingletonObjects). When Stage 7 is reached later, Spring recognizes the existing proxy and avoids duplicate wrapping.

4. In what exact order are destruction methods invoked upon context shutdown, and why does Spring ignore prototypes?

Answer: Upon graceful context shutdown (context.close()), destruction callbacks execute in the exact reverse order of initialization:

  1. Methods marked with @PreDestroy (via InitDestroyAnnotationBeanPostProcessor).
  2. The destroy() method of the DisposableBean interface.
  3. Custom destroyMethod declared in @Bean(destroyMethod = "...").

Spring completely ignores prototype-scoped beans during destruction. After completing the initialization phase and returning the prototype instance to the caller, Spring drops all references to the prototype to prevent container memory leaks. Managing resource teardown for prototype beans is strictly the caller’s responsibility.


🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“The Spring Bean Lifecycle comprises 12 canonical stages:

  1. Instantiation via constructor.
  2. Property Population (populateBean).
  3. Aware Interfaces (BeanNameAware, ApplicationContextAware).
  4. BPP Before Initialization (@PostConstruct).
  5. InitializingBean.afterPropertiesSet().
  6. Custom initMethod.
  7. BPP After Initialization (Creation of AOP Dynamic Proxies for @Transactional / @Async).
  8. In Service.
  9. Context Shutdown.
  10. @PreDestroy.
  11. DisposableBean.destroy().
  12. Custom destroyMethod.

Proxies are generated in Step 7, which explains why @Transactional does not function inside @PostConstruct.”


Red Flags & Pitfalls (DO NOT Say)

  • ❌ “AOP dynamic proxies are generated inside the class constructor.” (Proxies are created in Step 7 during postProcessAfterInitialization).
  • ❌ “Spring invokes @PreDestroy on prototype beans when the container shuts down.” (Spring drops prototype tracking after Step 7; cleanup is the caller’s responsibility).
  • ❌ “It is safe to inject business repositories directly into custom BeanPostProcessors.” (This triggers premature bean creation and strips the repository of AOP proxy support).
  • ❌ “InitializingBean runs before @PostConstruct.” (@PostConstruct executes first inside BPP Before; InitializingBean runs second).

Comprehensive 12-Stage Lifecycle Reference

Lifecycle Execution Order Summary:
┌────────┬──────────────────────────────────────────┬────────────────────────────────────────┐
│ Step   │ Hook / Method                            │ Responsible Engine Component           │
├────────┼──────────────────────────────────────────┼────────────────────────────────────────┤
│ 1      │ Instantiation                            │ Constructor.newInstance()              │
│ 2      │ Property Population (DI)                 │ AutowiredAnnotationBeanPostProcessor   │
│ 3      │ Aware Callbacks                          │ invokeAwareMethods()                   │
│ 4      │ @PostConstruct                           │ CommonAnnotationBeanPostProcessor      │
│ 5      │ InitializingBean.afterPropertiesSet()    │ invokeInitMethods()                    │
│ 6      │ Custom initMethod                        │ invokeInitMethods()                    │
│ 7      │ AOP Dynamic Proxy Generation             │ AbstractAutoProxyCreator (BPP After)   │
│ READY  │ Bean In Service                          │ Registered in singletonObjects         │
│ 8      │ Context Shutdown Triggered               │ AbstractApplicationContext.close()     │
│ 9      │ @PreDestroy                              │ InitDestroyAnnotationBeanPostProcessor │
│ 10     │ DisposableBean.destroy()                 │ DisposableBeanAdapter                  │
│ 11     │ Custom destroyMethod                     │ DisposableBeanAdapter                  │
└────────┴──────────────────────────────────────────┴────────────────────────────────────────┘