Spring Bean Life Cycle: Callbacks, Order and Diagram

The Spring bean life cycle is the fixed order in which Spring creates a bean, injects its dependencies, runs its init callbacks and, when the context closes, its destroy callbacks.

Spring-bean-life-cycle

The Spring bean life cycle is the fixed sequence of steps that the Spring container runs for every bean, from creating the object and injecting its dependencies to calling its cleanup code when the container closes. We can hook our own code into two points of that sequence, namely right after the bean is ready (initialization callbacks) and right before Spring removes it (destruction callbacks).

We use these callbacks for setup and cleanup work that the constructor cannot do, such as checking injected dependencies, loading a cache at startup, starting a scheduler, or closing connections and files at shutdown.

The following example is a Spring Boot 4.1.1 component with one callback of each kind. In a Spring Boot application, both annotations work with no extra setup.

@Component
class PlaylistCache {

  @PostConstruct
  void load() {
    System.out.println("PlaylistCache @PostConstruct");   // runs once, after dependency injection
  }

  @PreDestroy
  void clear() {
    System.out.println("PlaylistCache @PreDestroy");      // runs once, when the context closes
  }
}

Notice that we never call load() or clear() ourselves. Spring calls load() during startup, and it calls clear() when the application context closes, which in Spring Boot happens from a JVM shutdown hook.

Next, we go through all 13 steps of the life cycle with real output, and the four ways to add callbacks, namely annotations, interfaces, custom init and destroy methods, and Aware interfaces. After that, we cover the shutdown hook, prototype beans and the questions that come up most often.

1. What is Lifecycle of a Bean in Spring?

The lifecycle of a bean in the Spring Framework refers to the series of steps and methods that a bean goes through from its instantiation to its eventual destruction.

The Spring container (an ApplicationContext, which builds on the BeanFactory) runs these steps. It creates, configures and destroys beans according to the bean scope (singleton, prototype, etc.). For a singleton bean, the steps run in the order shown in the diagram.

Spring bean life cycle diagram in two columns. Startup steps 1 to 9 on the left are Constructor, Dependency injection, BeanNameAware.setBeanName(), ApplicationContextAware.setApplicationContext(), BeanPostProcessor before initialization, @PostConstruct method, InitializingBean.afterPropertiesSet(), custom init method and BeanPostProcessor after initialization. An arrow leads to step 10, bean is ready and in use. After context.close() or the JVM shutdown hook come step 11 @PreDestroy method, step 12 DisposableBean.destroy() and step 13 custom destroy method. A note says prototype beans get steps 1 to 9 only.
Spring creates and initializes the bean in steps 1 to 9, and it runs the destruction callbacks 11 to 13 only when the context closes.

We can group the 13 steps into four phases, and each phase has its own hooks for our code.

PhaseStepsWhat happensOur hook
Creation1-2Spring calls the constructor and injects the dependencies.constructor, setters
Aware callbacks3-4Spring passes infrastructure objects such as the bean name.Aware interfaces
Initialization5-9Spring runs the post processors and the init callbacks.@PostConstruct, afterPropertiesSet(), init method, BeanPostProcessor
Destruction11-13Spring runs the destroy callbacks when the context closes.@PreDestroy, destroy(), destroy method

A BeanPostProcessor is a special bean that Spring calls for every other bean, once before and once after the init callbacks. Spring uses its own post processors for many features, for example it calls @PostConstruct methods from one of them, and it creates AOP proxies in the “after initialization” step.

2. Bean Lifecycle Callback Methods

Spring lets a bean run its own code at two points of the life cycle, so the callback methods fall into two groups.

  • Post-initialization callback methods run once the bean is created and all dependencies are injected.
  • Pre-destruction callback methods run when the container closes.

In Spring, the initialization life-cycle callback methods will be called on objects regardless of bean scope, while for beans with prototype scope, the destruction life-cycle callback methods will not be called.

It’s the responsibility of the application to manage the destruction and cleanup of prototype beans, as Spring does not keep a reference to prototype beans for automatic destruction. We cover the cleanup options for prototype beans in section 10.2.

2.1. Post-Initialization Callbacks

Most often, but not limited to, the initialization callbacks are used to perform the dependency checks that are not possible in the constructor. With setter or field injection, the dependencies are still null inside the constructor, and Spring sets them before the init callback runs. Other use cases are loading a cache and starting a scheduler.

For example, a music app keeps the 500 most played songs in memory. The PlaylistService bean loads them from the database in its init callback, so the first request finds the cache full.

The post-initialization callback method cannot accept any arguments. A @PostConstruct method with a parameter makes the context fail at startup with IllegalStateException: Lifecycle annotation requires a no-arg method.

We can even define all initialization mechanisms on the same bean instance. It is allowed in Spring. In this case, Spring invokes the callbacks in the following order.

  1. Method annotated with @PostConstruct
  2. Method InitializingBean.afterPropertiesSet()
  3. Method specified in the @Bean(initMethod = “…”) definition or the XML init-method attribute

Spring runs the init callbacks inside the container’s singleton creation lock. So we keep them short, and we move slow work, such as warming a large cache over the network, to a ContextRefreshedEvent listener or to SmartInitializingSingleton.afterSingletonsInstantiated().

2.2. Pre-destruction Callbacks

When the application shuts down, Spring calls the ConfigurableBeanFactory.destroySingletons()) method which provides us a chance to clean up any resources that the beans might be holding open, so the application shuts down gracefully. When the destroySingletons() is called, the beans get the notifications via pre-destruction callback methods (similar to post-initialization callbacks).

The destruction callback is often used in conjunction with the initialization callback. In many cases, we create and configure a resource in the initialization callback and release the resource in the destruction callback.

Notice the method name destroySingletons(). It hints that beans with other scopes than singleton do not have their life cycle fully managed by Spring.

As with the case of bean creation, the order of method invocations in case of multiple bean destruction callbacks is as follows.

  1. Method annotated with @PreDestroy
  2. Method DisposableBean.destroy()
  3. Method specified in the @Bean(destroyMethod = “…”) definition or the XML destroy-method attribute

3. Spring Bean Life Cycle Example With All 13 Steps

The order of the steps is easier to remember once we see it printed. So we put every callback on one bean and print a numbered line from each of them.

The following example is a PlaylistService bean that gets a SongStore dependency through a setter and loads the songs at startup. The bean implements BeanNameAware, ApplicationContextAware, InitializingBean and DisposableBean, and it also has @PostConstruct and @PreDestroy methods. The example uses Spring Framework 7.0.9 without Spring Boot, jakarta.annotation-api 3.0.0 and Java 25, and the complete code is in the spring-core repository.

public class PlaylistService implements BeanNameAware, ApplicationContextAware,
    InitializingBean, DisposableBean {

  private SongStore songStore;
  private final List<String> songs = new ArrayList<>();

  public PlaylistService() {
    System.out.println("1. Constructor");
  }

  @Autowired
  public void setSongStore(SongStore songStore) {
    this.songStore = songStore;
    System.out.println("2. Setter injection: setSongStore()");
  }

  @Override
  public void setBeanName(String name) {
    System.out.println("3. BeanNameAware.setBeanName(): " + name);
  }

  @Override
  public void setApplicationContext(ApplicationContext context) {
    System.out.println("4. ApplicationContextAware.setApplicationContext()");
  }

  @PostConstruct
  public void checkStore() {
    if (songStore == null) {
      throw new IllegalStateException("songStore is required");
    }
    System.out.println("6. @PostConstruct checkStore()");
  }

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

  public void loadSongs() {
    songs.addAll(songStore.findAll());
    System.out.println("8. Init method loadSongs(): " + songs.size() + " songs");
  }

  @PreDestroy
  public void saveState() {
    System.out.println("11. @PreDestroy saveState()");
  }

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

  public void clearSongs() {
    songs.clear();
    System.out.println("13. Destroy method clearSongs()");
  }
}

The configuration class registers PlaylistService with the names of its init and destroy methods. It also registers a small LoggingPostProcessor, which prints steps 5 and 9 for PlaylistService only. We declare the post processor @Bean method as static, because Spring must create post processors before all other beans.

@Configuration
public class AppConfig {

  @Bean
  static LoggingPostProcessor loggingPostProcessor() {
    return new LoggingPostProcessor();
  }

  @Bean
  SongStore songStore() {
    return new SongStore();
  }

  @Bean(initMethod = "loadSongs", destroyMethod = "clearSongs")
  PlaylistService playlistService() {
    return new PlaylistService();
  }
}

public class LoggingPostProcessor implements BeanPostProcessor {

  @Override
  public Object postProcessBeforeInitialization(Object bean, String beanName) {
    if (bean instanceof PlaylistService) {
      System.out.println("5. BeanPostProcessor.postProcessBeforeInitialization()");
    }
    return bean;
  }

  @Override
  public Object postProcessAfterInitialization(Object bean, String beanName) {
    if (bean instanceof PlaylistService) {
      System.out.println("9. BeanPostProcessor.postProcessAfterInitialization()");
    }
    return bean;
  }
}

The main() method opens the context in a try-with-resources block. The block closes the context at its end, even when the code inside throws an exception, so the destroy callbacks always run.

try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
  PlaylistService service = context.getBean(PlaylistService.class);
  System.out.println("10. Bean in use: " + service.getSongs());
}

The console shows all 13 steps in the order of the diagram in section 1.

1. Constructor
2. Setter injection: setSongStore()
3. BeanNameAware.setBeanName(): playlistService
4. ApplicationContextAware.setApplicationContext()
5. BeanPostProcessor.postProcessBeforeInitialization()
6. @PostConstruct checkStore()
7. InitializingBean.afterPropertiesSet()
8. Init method loadSongs(): 3 songs
9. BeanPostProcessor.postProcessAfterInitialization()
10. Bean in use: [Intro, Blue, Night Drive]
11. @PreDestroy saveState()
12. DisposableBean.destroy()
13. Destroy method clearSongs()

We can see that the dependency is already injected in step 2, so the null check in checkStore() passes. In a real bean, we pick one init mechanism and one destroy mechanism, because three callbacks for the same job make the code harder to follow.

4. Implementing the InitializingBean and DisposableBean Interfaces

The org.springframework.beans.factory.InitializingBean interface allows a bean to perform initialization work after all necessary properties on the bean have been set by the container. The InitializingBean interface specifies a single method afterPropertiesSet() which Spring calls after the bean’s properties have been set.

public class PlaylistService implements InitializingBean {

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

Similarly, implementing the org.springframework.beans.factory.DisposableBean interface allows a bean to get a callback during bean destruction, such as when the application context is closed. The DisposableBean interface specifies a single method destroy(), which Spring calls when it destroys the bean.

public class PlaylistService implements DisposableBean {

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

The Spring team recommends not to use InitializingBean, because it couples our class to Spring interfaces. The annotations from section 7 and the custom methods from section 6 do the same job without that coupling. The interfaces still make sense in framework code, and we see them in many Spring classes.

5. Aware Interfaces to Add Specific Behavior

Spring offers a range of interfaces that allow the beans to tell the container that they require a particular infrastructure dependency. Each of these Aware interfaces requires us to implement one setter method, and Spring calls it before the init callbacks, as steps 3 and 4 of the example show.

Some commonly used Aware interfaces are listed in the table.

Aware interfaceMethod to OverridePurpose
ApplicationContextAwaresetApplicationContext()Sets the ApplicationContext that the bean runs in.
ApplicationEventPublisherAwaresetApplicationEventPublisher()Sets the ApplicationEventPublisher that the bean runs in.
BeanNameAwaresetBeanName()Sets the name of the bean in the bean factory.
LoadTimeWeaverAwaresetLoadTimeWeaver()Sets the LoadTimeWeaver of this object’s containing ApplicationContext.
MessageSourceAwaresetMessageSource()Sets the MessageSource that this bean runs in.
NotificationPublisherAwaresetNotificationPublisher()Sets the NotificationPublisher instance for the current managed resource instance.
ResourceLoaderAwaresetResourceLoader()Sets the ResourceLoader that this object runs in.
ServletContextAwaresetServletContext()Sets the ServletContext that this object runs in.

A simple Java bean definition shows how to use the Aware interfaces. The bean keeps the ApplicationContext in a field and can use it in other methods.

public class DemoBean implements ApplicationContextAware {

  private ApplicationContext ctx;

  @Override
  public void setApplicationContext(ApplicationContext ctx) throws BeansException {
    this.ctx = ctx;
  }

  //Use the context in other bean methods
}

In application code, we can inject most of these objects with @Autowired or constructor injection instead, e.g. ApplicationContext, ApplicationEventPublisher and ResourceLoader. The Aware interfaces remain the way to get the bean’s own name.

6. Custom init() and destroy() Methods

Custom init and destroy methods are plain methods with any name, so the bean class does not depend on Spring at all. We can add them in three ways.

  • Local definitions apply to a single bean.
  • Global definitions apply to all beans defined in the whole XML file.
  • Inferred destroy methods are found by Spring on its own, when the bean has a public close() or shutdown() method.

6.1. Bean-specific Init and Destroy Methods

In Java configuration, we pass the method names to the @Bean annotation.

@Bean(initMethod = "loadSongs", destroyMethod = "clearSongs")
PlaylistService playlistService() {
  return new PlaylistService();
}

The local init and destroy methods can be configured for a specific bean with the init-method and destroy-method attributes in XML configuration.

<bean id="playlistService" class="com.howtodoinjava.core.lifecycle.PlaylistService"
      init-method="loadSongs" destroy-method="clearSongs">
  <property name="songStore" ref="songStore"/>
</bean>

The init and destroy methods are defined in the bean as ordinary public methods with no parameters.

public void loadSongs() {
  songs.addAll(songStore.findAll());
  System.out.println("8. Init method loadSongs(): " + songs.size() + " songs");
}

public void clearSongs() {
  songs.clear();
  System.out.println("13. Destroy method clearSongs()");
}

When the method name has a typo, such as initMethod = “loadSong”, Spring stops at startup with BeanDefinitionValidationException: Could not find an init method named ‘loadSong’ on bean with name ‘playlistService’, so the typo shows up on the first run.

6.2. Global-scoped Init and Destroy Methods

We can define the init and destroy methods at the global level as well. The container invokes the global init and destroy methods for all bean definitions in the XML file using the attributes default-init-method and default-destroy-method of the beans element.

Global overrides are helpful when we have a pattern of defining common method names such as init() and cleanup() for all the beans consistently. This feature helps us not to duplicate the init and destroy methods for all beans, separately.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
         https://www.springframework.org/schema/beans/spring-beans.xsd"
       default-init-method="init" default-destroy-method="cleanup">

  <bean id="reportJob" class="com.howtodoinjava.core.lifecycle.ReportJob"/>
</beans>

The beans that want the callbacks declare methods with these names. A bean without an init() or cleanup() method is skipped, with no error, so playlistService in the same file is not affected.

public class ReportJob {
  public void init() {
    System.out.println("ReportJob.init()");
  }
  public void cleanup() {
    System.out.println("ReportJob.cleanup()");
  }
}

An XML file without Java config turns off annotation processing. With the full lifecycle-beans.xml file, Spring calls init(), cleanup(), afterPropertiesSet() and the XML init and destroy methods, but not @PostConstruct or @PreDestroy. To turn on the annotations in an XML file, we add the context:annotation-config element.

6.3. Inferred close() and shutdown() Methods

For @Bean methods, Spring looks for a public close() or shutdown() method on the bean and calls it as the destroy method. This is the default, so beans that implement AutoCloseable, such as a DataSource or a file reader, get closed with no configuration.

public class SongFileReader implements AutoCloseable {

  @Override
  public void close() {
    System.out.println("SongFileReader.close()");   // called when the context closes
  }
}

@Bean
SongFileReader songFileReader() {
  return new SongFileReader();
}

In XML, we get the same behavior with destroy-method=”(inferred)”. When a bean has a close() method that Spring must not call, for example because another component owns the resource, we turn off the inference with @Bean(destroyMethod = “”).

7. @PostConstruct and @PreDestroy Annotations

The JSR-250 annotations @PostConstruct and @PreDestroy are the recommended way to add life cycle callbacks, because the bean class does not depend on Spring interfaces.

  • A method annotated with @PostConstruct is invoked after Spring has created the bean and injected all its dependencies, and before Spring hands the bean to other beans.
  • A method annotated with @PreDestroy is invoked when Spring is about to destroy the bean, which happens when the container closes.

Since Spring Framework 6, both annotations come from the jakarta.annotation package. Spring Boot’s spring-boot-starter already includes jakarta.annotation-api, whereas a Spring application without Spring Boot needs the dependency in its pom.xml.

<dependency>
  <groupId>jakarta.annotation</groupId>
  <artifactId>jakarta.annotation-api</artifactId>
  <version>3.0.0</version>
</dependency>

The ShoppingCart bean uses both annotations. We use the same class as a prototype bean in section 10.2.

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

public class ShoppingCart {

  @PostConstruct
  public void init() {
    System.out.println("ShoppingCart @PostConstruct");
  }

  @PreDestroy
  public void cleanup() {
    System.out.println("ShoppingCart @PreDestroy");
  }
}

Old code that imports javax.annotation.PostConstruct does not work in Spring Framework 7. Spring 7 never calls a method with the javax annotation, and it logs no warning. When we migrate an app from Spring Boot 2.x, we replace every javax.annotation import with jakarta.annotation.

8. Registering a Shutdown Hook is Necessary

When we run the demo programs from the previous sections, we expect the program to print the messages written in post-init and pre-destroy callback methods. Let’s try it with a context that we never close.

var context = new AnnotationConfigApplicationContext(AppConfig.class);

The program output prints only the init messages, steps 1 to 9. The destroy callback messages are not printed.

1. Constructor
...
8. Init method loadSongs(): 3 songs
9. BeanPostProcessor.postProcessAfterInitialization()

This is because, to call the pre-destroy callbacks, we must call the close() method of the context before the application stops. But when an application can be closed, stopped or killed in several ways, it becomes harder to place the close() call.

Fortunately, there is a solution. Java allows us to register a shutdown hook, which is a thread that is executed before the application shuts down.

To take advantage of this mechanism, we must invoke the registerShutdownHook() method of ConfigurableApplicationContext anytime before the application is stopped. Let’s try this in our demo program.

var context = new AnnotationConfigApplicationContext(AppConfig.class);
context.registerShutdownHook();

The program output prints all the init and destroy messages this time, which is the expected behavior.

1. Constructor
...
9. BeanPostProcessor.postProcessAfterInitialization()
11. @PreDestroy saveState()
12. DisposableBean.destroy()
13. Destroy method clearSongs()

The JVM runs shutdown hooks on a normal exit, on Ctrl+C and on a kill PID (SIGTERM). The JVM does not run them on kill -9 or a power loss, so no destroy callback can be a replacement for data that must be saved at once.

8.1. Bean Life Cycle in Spring Boot

In a Spring Boot application, we don’t register the hook ourselves. Every SpringApplication registers a JVM shutdown hook by default, so the context closes and the destroy callbacks run when the app stops. The life cycle steps themselves are the same as in Spring Framework, because Spring Boot uses the same container.

The following example is the PlaylistCache component from the intro, started with SpringApplication.run() in Spring Boot 4.1.1. The main() method prints a line after run() returns.

@SpringBootApplication
public class BootApp {
  public static void main(String[] args) {
    SpringApplication.run(BootApp.class, args);
    System.out.println("main() returned");
  }
}

The app has no web server, so the JVM exits after main() returns, and the shutdown hook closes the context.

PlaylistCache @PostConstruct
main() returned
PlaylistCache @PreDestroy

The property spring.main.register-shutdown-hook=false turns the hook off. With that property, the same app prints only the first two lines, so we set it only when our own code closes the context.

9. Which Life Cycle Callback Should We Use?

All four ways run at a known point of the life cycle, so the choice depends on who owns the bean class and how the bean is registered.

WayCoupled to SpringWorks for classes we cannot changeTypical use
@PostConstruct / @PreDestroyno (Jakarta annotations)noour own @Component and @Service classes
@Bean(initMethod, destroyMethod)noyesthird-party classes registered in a @Configuration class
Inferred close() / shutdown()noyesAutoCloseable resources such as pools and clients
InitializingBean / DisposableBeanyesnoframework and library code

For our own @Component classes, the annotations are the simplest choice. For a third-party class, such as a client from a library, we cannot add annotations, so we register it with a @Bean method and name its init and destroy methods there.

10. Spring Bean Life Cycle FAQs

10.1. What Is the Difference Between the Constructor and @PostConstruct?

The constructor runs before Spring injects setter and field dependencies, whereas @PostConstruct runs after all dependencies are injected. So code that uses an @Autowired field belongs in @PostConstruct. With constructor injection, the dependencies are available in the constructor too, so either place works for code that needs only those dependencies.

10.2. Why Is @PreDestroy Not Called for Prototype Beans?

Spring creates a prototype bean, runs its init callbacks and hands it over, but it keeps no reference to it. So Spring has nothing to destroy when the context closes. When a prototype bean holds a resource, our code destroys it by calling destroyBean(), which runs the @PreDestroy method and DisposableBean.destroy().

try (var context = new AnnotationConfigApplicationContext(ScopeConfig.class)) {
  ShoppingCart cart = context.getBean(ShoppingCart.class);   // ShoppingCart @PostConstruct
  context.getBeanFactory().destroyBean(cart);                 // ShoppingCart @PreDestroy
}

Without the destroyBean() call, the program prints only ShoppingCart @PostConstruct, even though the context closes.

10.3. Why Is @PreDestroy Not Called at All?

We check the common causes in this order.

  • The context is never closed. A plain main() method needs try-with-resources, close() or registerShutdownHook(), as we saw in section 8.
  • The bean has prototype scope, as section 10.2 shows.
  • The import is javax.annotation.PreDestroy, which Spring Framework 7 ignores.
  • The process is killed with kill -9, so the JVM runs no shutdown hooks.
  • A Spring application without Spring Boot has no jakarta.annotation-api on the classpath, or an XML configuration has no context:annotation-config element.

10.4. Is BeanPostProcessor Part of the Bean Life Cycle?

Yes. A BeanPostProcessor runs for every bean in steps 5 and 9 of the life cycle, once before and once after the init callbacks. We write one when the same logic must apply to many beans, for example to wrap them in a proxy, whereas a @PostConstruct method belongs to one bean only.

11. Conclusion

The Spring bean life cycle has a fixed order. Spring creates the bean, injects the dependencies and calls the Aware methods. After that, it runs the post processors and the init callbacks in the order @PostConstruct, afterPropertiesSet() and the custom init method.

The destroy callbacks follow the same pattern with @PreDestroy, destroy() and the custom destroy method. They run only for singleton beans, and only when the context closes, which Spring Boot does from its shutdown hook and a plain Spring application must do with close() or registerShutdownHook().

For our own classes, @PostConstruct and @PreDestroy are the recommended choice, and for third-party classes, @Bean(initMethod, destroyMethod) does the same job. The life cycle order is also a commonly asked Spring interview question.

12. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.