Adetayo Akinsanya unkletayo.dev

The Complete Spring Bean Lifecycle: Instantiation, Dependency Injection, Init, and Destroy

Understanding Aware interfaces, BeanPostProcessors, @PostConstruct, and AOP proxying phases.

Part 7 in Series — Catch up on the previous article: Component Scanning Mechanics: Annotations, Metadata Readers, and ASM Bytecode Parsing (Part 6) before diving into this post.

A backend developer creates a database cache manager bean and attempts to initialize a cache warm-up query inside the class constructor:

@Component
public class CacheManager {

    @Autowired
    private DatabaseClient databaseClient; // Null during constructor execution!

    public CacheManager() {
        // CONSTRUCTOR EXECUTION:
        System.out.println("Warming up cache...");
        databaseClient.queryAllRecords(); // Throws NullPointerException!
    }
}

When the application boots, the JVM throws a NullPointerException on startup.

The developer is confused: why is databaseClient null when accessed inside the constructor of an @Autowired class?

Because in the Spring Bean Lifecycle, constructor execution occurs BEFORE dependency injection takes place.

Moving the cache warm-up method into a @PostConstruct method solves the issue:

@PostConstruct
public void initCache() {
    // Executes AFTER dependency injection completes!
    databaseClient.queryAllRecords(); // Runs smoothly!
}

How does Spring transition a raw BeanDefinition recipe into a fully-wired, proxied, production-ready bean?

To avoid subtle initialization bugs, we must trace the 10 Sequential Phases of the Spring Bean Lifecycle.


1. The Complete Bean Lifecycle Flowchart

When ApplicationContext initializes a bean, it executes a strict, multi-stage lifecycle pipeline:

[ 1. BeanDefinition Parsing ]
              |
              v
[ 2. Instantiation ] -------------------> Constructor executed (Fields are still NULL!)
              |
              v
[ 3. Populate Properties ] -------------> Dependency Injection (@Autowired fields populated)
              |
              v
[ 4. Aware Interfaces ] ----------------> BeanNameAware, ApplicationContextAware callbacks
              |
              v
[ 5. BeanPostProcessor Before ] --------> postProcessBeforeInitialization()
              |
              v
[ 6. Initialization Callbacks ] --------> @PostConstruct -> InitializingBean -> init-method
              |
              v
[ 7. BeanPostProcessor After ] ---------> postProcessAfterInitialization() (AOP PROXY WRAPPING!)
              |
              v
[ 8. READY FOR USE ] -------------------> Application processes requests using bean/proxy
              |
              v
[ 9. Destruction Callbacks ] -----------> @PreDestroy -> DisposableBean -> destroy-method

2. Detailed Breakdown of Lifecycle Phases

Phase 1: Instantiation (Constructor Execution)

Spring selects an appropriate constructor and invokes newInstance().

  • Critical Rule: During constructor execution, NO @Autowired fields, @Value properties, or dependencies have been injected yet. All fields contain default values (null, 0, false).

Phase 2: Populate Properties (Dependency Injection)

Spring uses AutowiredAnnotationBeanPostProcessor to inject references into fields annotated with @Autowired or @Value.

  • After this phase completes, all declared dependencies are non-null and accessible.

Phase 3: Aware Interface Callbacks

If the bean implements Spring Aware marker interfaces, Spring injects framework infrastructure references:

  • BeanNameAware: Passes the bean’s registered string ID (setBeanName).
  • BeanFactoryAware: Passes the owning BeanFactory instance.
  • ApplicationContextAware: Passes the active ApplicationContext instance.

Phase 4: BeanPostProcessor.postProcessBeforeInitialization()

Spring iterates through all registered BeanPostProcessor instances in the container and calls postProcessBeforeInitialization(bean, beanName).

  • Common Use Case: CommonAnnotationBeanPostProcessor scans for @PostConstruct annotations and executes annotated methods here!

Phase 5: Initialization Callbacks

Spring executes custom initialization logic across three supported interfaces in order:

  1. @PostConstruct Method: Standard JSR-250 annotation. (Recommended standard).
  2. InitializingBean.afterPropertiesSet(): Spring interface callback.
  3. Custom init-method: Configured via @Bean(initMethod = "customInit").

Phase 6: BeanPostProcessor.postProcessAfterInitialization()

This is one of the most important phases in Spring Core.

  • AOP Dynamic Proxy Generation: Frameworks like Spring AOP, @Transactional, and @Async wrap the target bean instance inside a CGLIB or JDK Dynamic Proxy during this phase!
  • Result: The object returned by ApplicationContext.getBean() is often not the raw bean instance, but an AOP proxy wrapping the bean!

Phase 7: Ready for Use & Destruction Callbacks

The bean (or proxy) actively serves application requests until the container shuts down (context.close()), triggering destruction callbacks in order:

  1. @PreDestroy Method: Executes cleanup hooks (closing network sockets, flushing logs).
  2. DisposableBean.destroy(): Spring interface destruction callback.
  3. Custom destroy-method: Configured via @Bean(destroyMethod = "cleanup").

3. Demonstrating Lifecycle Order in Code

@Component
public class LifecycleDemoBean implements BeanNameAware, InitializingBean, DisposableBean {

    @Autowired
    private DependencyService dependencyService;

    // 1. Instantiation
    public LifecycleDemoBean() {
        System.out.println("1. Constructor Executed (dependencyService = " + dependencyService + ")");
    }

    // 3. Aware Callback
    @Override
    public void setBeanName(String name) {
        System.out.println("3. BeanNameAware Called (Bean Name: " + name + ")");
    }

    // 5. @PostConstruct Callback
    @PostConstruct
    public void postConstruct() {
        System.out.println("5. @PostConstruct Executed (dependencyService = " + dependencyService.getClass().getSimpleName() + ")");
    }

    // 6. InitializingBean Callback
    @Override
    public void afterPropertiesSet() {
        System.out.println("6. InitializingBean.afterPropertiesSet() Executed");
    }

    // 9. @PreDestroy Callback
    @PreDestroy
    public void preDestroy() {
        System.out.println("9. @PreDestroy Executed");
    }

    // 10. DisposableBean Callback
    @Override
    public void destroy() {
        System.out.println("10. DisposableBean.destroy() Executed");
    }
}

Execution Console Output:

1. Constructor Executed (dependencyService = null)
2. Dependency Injection Completed (@Autowired fields populated)
3. BeanNameAware Called (Bean Name: lifecycleDemoBean)
4. BeanPostProcessor.postProcessBeforeInitialization()
5. @PostConstruct Executed (dependencyService = DependencyService)
6. InitializingBean.afterPropertiesSet() Executed
7. BeanPostProcessor.postProcessAfterInitialization() (Proxy Wrapping)
... [Application Running] ...
9. @PreDestroy Executed
10. DisposableBean.destroy() Executed

Summary & Next Steps

Understanding the Spring Bean Lifecycle prevents initialization bugs and clarifies framework behavior:

  • Constructors execute during initial instantiation when @Autowired dependencies are still null.
  • Property Population injects dependencies into fields after constructor completion.
  • @PostConstruct executes after dependency injection completes, serving as the correct location for initialization logic.
  • BeanPostProcessor.postProcessAfterInitialization() wraps target beans inside AOP dynamic proxies for @Transactional and @Async support.
  • @PreDestroy executes graceful cleanup operations during container shutdown.

In the next article, we open Module 3 with Why Spring Boot Exists: Eliminating XML Configuration and Dependency Hell.

References & Further Reading

  1. Kiczales, G., et al. (1997). Aspect-Oriented Programming. ECOOP ‘97 Conference Proceedings, Springer.
  2. ByteBuddy Project. Runtime Code Generation for the Java Virtual Machine. ByteBuddy Docs.
  3. Spring.io. Aspect Oriented Programming with Spring. Spring Docs.

Up Next in Series →

Part 8: Why Spring Boot Exists: Eliminating XML Configuration and Dependency Hell

Continue to Part 8 →