Course topics

By WebNest Studio Engineering Team

Spring Framework Tutorial

Spring Bean Lifecycle

A Spring bean does not simply appear fully formed — the container walks it through a well-defined sequence of steps from instantiation to eventual destruction. Understanding this lifecycle matters whenever a bean needs to perform setup work after all its dependencies are injected (like opening a connection pool) or cleanup work before the application shuts down (like closing that pool).

Spring exposes several ways to hook into this lifecycle: the @PostConstruct and @PreDestroy annotations (the modern, framework-agnostic approach from Jakarta/Java EE), and the InitializingBean and DisposableBean interfaces (Spring's original, interface-based approach). Both accomplish the same goal, but differ in how tightly they couple your class to the Spring API.

The Full Lifecycle Sequence

In broad strokes, the container: (1) instantiates the bean by calling its constructor, (2) populates its properties via dependency injection, (3) calls any Aware interface methods (like BeanNameAware) if implemented, (4) invokes BeanPostProcessor "before initialization" callbacks, (5) calls the initialization callback (@PostConstruct or afterPropertiesSet()), (6) invokes BeanPostProcessor "after initialization" callbacks, and finally the bean is ready for use. On container shutdown, destruction callbacks (@PreDestroy or destroy()) run in reverse.

@PostConstruct and @PreDestroy

@PostConstruct marks a method to be run once, right after dependency injection is complete, making it the ideal place for setup logic that depends on injected collaborators being available. @PreDestroy marks a method to run just before the container destroys the bean (for singleton beans, this happens on graceful context shutdown). These annotations come from the Jakarta Annotations specification, not Spring itself, which keeps the annotated class portable across frameworks.

InitializingBean and DisposableBean

Implementing InitializingBean requires overriding afterPropertiesSet(), called at the same point in the lifecycle as @PostConstruct. Implementing DisposableBean requires overriding destroy(), called at the same point as @PreDestroy. These interfaces tie the class directly to Spring's API, which is why @PostConstruct/@PreDestroy are generally preferred in modern code — but the interface approach avoids any dependency on annotation processing and can be marginally faster since Spring checks the interface directly instead of scanning for annotations.

  • @PostConstruct / @PreDestroy — annotation-based, framework-agnostic, the common modern choice.
  • InitializingBean.afterPropertiesSet() / DisposableBean.destroy() — interface-based, tightly coupled to Spring.
  • Prototype-scoped beans receive initialization callbacks but NOT destruction callbacks from the container automatically.

Example

A bean using both @PostConstruct/@PreDestroy and the Spring lifecycle interfaces

Java
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

class ConnectionPool implements InitializingBean, DisposableBean {

    @PostConstruct
    public void openPool() {
        System.out.println("1. @PostConstruct: pool opened");
    }

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

    @PreDestroy
    public void closePool() {
        System.out.println("3. @PreDestroy: pool closing");
    }

    @Override
    public void destroy() {
        System.out.println("4. DisposableBean: destroy");
    }
}

@Configuration
class AppConfig {
    @Bean
    public ConnectionPool connectionPool() {
        return new ConnectionPool();
    }
}

public class LifecycleDemo {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context =
            new AnnotationConfigApplicationContext(AppConfig.class);
        context.close(); // triggers destruction callbacks
    }
}
Output
1. @PostConstruct: pool opened
2. InitializingBean: afterPropertiesSet
3. @PreDestroy: pool closing
4. DisposableBean: destroy

Common Mistakes

  • Expecting @PreDestroy or destroy() to run for prototype-scoped beans — Spring does not track prototype beans after handing them out, so it never calls their destruction callbacks.
  • Doing dependency-dependent setup work inside the constructor instead of @PostConstruct — at constructor time, injected fields set via setter injection may not yet be populated.
  • Forgetting to call context.close() (or use try-with-resources / a shutdown hook) in a standalone application, so @PreDestroy callbacks never fire.
  • Assuming @PostConstruct runs before dependency injection completes — it actually runs after all dependencies have been set.

Exercise

Using the lesson's A bean using both @PostConstruct/@PreDestroy and the Spring lifecycle interfaces example, create a small implementation or design sketch that demonstrates Lifecycle order: construct, inject dependencies, run init callbacks, use the bean, run destroy callbacks on shutdown.. Add a test case or scenario that would catch Expecting @PreDestroy or destroy() to run for prototype-scoped beans — Spring does not track prototype beans after handing them out, so it never calls their destruction callbacks., and describe the expected result.

Show hints
  • Start with The Full Lifecycle Sequence and compare your approach with A bean using both @PostConstruct/@PreDestroy and the Spring lifecycle interfaces. Identify the decision or behavior the lesson recommends.
  • Walk through your scenario step by step. Confirm that it follows Lifecycle order: construct, inject dependencies, run init callbacks, use the bean, run destroy callbacks on shutdown. and that it would catch the failure described by Expecting @PreDestroy or destroy() to run for prototype-scoped beans — Spring does not track prototype beans after handing them out, so it never calls their destruction callbacks..

Quick quiz

1. Which choice is a mistake to avoid in Spring Bean Lifecycle?

  • A. Lifecycle order: construct, inject dependencies, run init callbacks, use the bean, run destroy callbacks on shutdown.
  • B. Expecting @PreDestroy or destroy() to run for prototype-scoped beans — Spring does not track prototype beans after handing them out, so it never calls their destruction callbacks.
  • C. Doing dependency-dependent setup work inside the constructor instead of @PostConstruct — at constructor time, injected fields set via setter injection may not yet be populated.
Show answer

B. Avoid this approach: Expecting @PreDestroy or destroy() to run for prototype-scoped beans — Spring does not track prototype beans after handing them out, so it never calls their destruction callbacks. The lesson recommends: Lifecycle order: construct, inject dependencies, run init callbacks, use the bean, run destroy callbacks on shutdown.

Interview questions

How would you apply the guidance in The Full Lifecycle Sequence to a real task, and what failure would you guard against?

Show answer

Lifecycle order: construct, inject dependencies, run init callbacks, use the bean, run destroy callbacks on shutdown. Apply this in The Full Lifecycle Sequence; the example A bean using both @PostConstruct/@PreDestroy and the Spring lifecycle interfaces shows one way to do it. Avoid Expecting @PreDestroy or destroy() to run for prototype-scoped beans — Spring does not track prototype beans after handing them out, so it never calls their destruction callbacks. and explain how your implementation or design prevents that failure.

Key Points to Remember

  • Lifecycle order: construct, inject dependencies, run init callbacks, use the bean, run destroy callbacks on shutdown.
  • @PostConstruct/@PreDestroy are the modern, framework-agnostic hooks; InitializingBean/DisposableBean are Spring-specific interfaces achieving the same result.
  • Destruction callbacks only run automatically for singleton-scoped beans, not prototype-scoped ones.
  • Calling context.close() (or a JVM shutdown hook) is required to trigger destroy callbacks in a standalone app.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a JDK project with their libraries on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering