Dependency Injection & Bean Lifecycle

Lifecycle Hooks

18 min Lesson 7 of 13

Lifecycle Hooks

Spring manages every bean from the moment it is instantiated to the moment the application context shuts down. Along that journey the container provides two well-defined hook points where your code can run: right after construction and dependency injection are complete (post-construct), and just before the bean is removed from the context (pre-destroy). Knowing how to use these hooks correctly — and which API to choose — is essential for writing beans that start reliably and clean up after themselves.

Why Lifecycle Hooks Matter

Consider a bean that wraps a database connection pool, or one that opens a file handle, or that must register itself with an external service at startup. None of that work belongs in the constructor — at that point Spring has not yet injected the bean's dependencies. The constructor must remain lightweight and side-effect-free. @PostConstruct gives you a guaranteed safe moment to run initialisation after all dependencies are fully wired.

The symmetric need exists at shutdown: connection pools must be drained, sockets must be closed, and external registrations must be revoked. @PreDestroy gives you a predictable moment to do all of that before the JVM exits.

@PostConstruct — Initialise After Wiring

@PostConstruct is a Jakarta EE annotation (package jakarta.annotation) supported by Spring out of the box. You place it on any public, package-private, or protected method that takes no arguments and returns void. Spring calls that method once, immediately after it has finished injecting all dependencies.

import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class ReportScheduler { @Value("${reports.outputDir}") private String outputDir; private java.nio.file.Path resolvedPath; @PostConstruct public void init() { // Safe: @Value has been injected before this runs resolvedPath = java.nio.file.Path.of(outputDir).toAbsolutePath().normalize(); if (!java.nio.file.Files.exists(resolvedPath)) { try { java.nio.file.Files.createDirectories(resolvedPath); } catch (java.io.IOException e) { throw new IllegalStateException("Cannot create output dir: " + resolvedPath, e); } } System.out.println("ReportScheduler ready. Output: " + resolvedPath); } }
Why not do this in the constructor? If you tried to read outputDir inside the constructor it would be null — Spring injects field values after the constructor returns. @PostConstruct is the earliest safe moment to use any injected value.

@PreDestroy — Clean Up Before Shutdown

@PreDestroy, from the same jakarta.annotation package, marks a method Spring will call just before destroying the bean — typically during ApplicationContext.close() or on JVM shutdown if the context registered a shutdown hook. The same signature rules apply: no arguments, void return type.

import jakarta.annotation.PreDestroy; import org.springframework.stereotype.Component; @Component public class ConnectionManager { private java.sql.Connection connection; @PostConstruct public void openConnection() throws java.sql.SQLException { connection = java.sql.DriverManager.getConnection( "jdbc:h2:mem:demo", "sa", ""); System.out.println("Connection opened"); } @PreDestroy public void closeConnection() { try { if (connection != null && !connection.isClosed()) { connection.close(); System.out.println("Connection closed cleanly"); } } catch (java.sql.SQLException e) { System.err.println("Error closing connection: " + e.getMessage()); } } }
@PreDestroy is NOT called for prototype-scoped beans. Spring does not track prototype instances after handing them out, so it has no way to call destroy callbacks on them. If you need cleanup for a prototype bean, you must manage its lifecycle manually or use a wrapper pattern.

InitializingBean and DisposableBean — The Interface Approach

Before @PostConstruct and @PreDestroy became mainstream, Spring provided the same hook points through two interfaces in org.springframework.beans.factory:

  • InitializingBean — declares afterPropertiesSet(), called after injection completes.
  • DisposableBean — declares destroy(), called before bean destruction.
import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Service; @Service public class CacheService implements InitializingBean, DisposableBean { private java.util.Map<String, Object> store; @Override public void afterPropertiesSet() { store = new java.util.concurrent.ConcurrentHashMap<>(); System.out.println("CacheService: in-memory store initialised"); } @Override public void destroy() { store.clear(); System.out.println("CacheService: store cleared on shutdown"); } public void put(String key, Object value) { store.put(key, value); } public Object get(String key) { return store.get(key); } }

Annotation vs Interface: Which Should You Choose?

The two mechanisms are functionally equivalent when used on singleton beans. The practical difference is one of coupling:

  • Prefer @PostConstruct / @PreDestroy in almost every new project. These are standard Jakarta EE annotations — your bean compiles and works even without Spring on the classpath, which makes it easier to unit-test in isolation.
  • Use the interfaces only when you are writing framework-level infrastructure code that deliberately targets the Spring API, or when you are maintaining legacy code that already uses them.
Spring also supports XML-level defaults (default-init-method) and per-bean init-method / destroy-method attributes in @Bean definitions. For a @Bean method you can write @Bean(initMethod = "start", destroyMethod = "stop") — useful when you do not own the class and cannot add annotations to it (for example, a third-party library).

The Exact Execution Order

When all three mechanisms are present on the same bean, Spring calls them in a defined sequence:

  1. Constructor
  2. Dependency injection (fields, setters)
  3. @PostConstruct method
  4. InitializingBean.afterPropertiesSet()
  5. @Bean(initMethod = ...) method

On shutdown the reverse order applies for destroy callbacks:

  1. @PreDestroy method
  2. DisposableBean.destroy()
  3. @Bean(destroyMethod = ...) method

In practice you rarely mix mechanisms on one bean, but understanding the order helps when you inherit a base class that already implements InitializingBean and you want to add a @PostConstruct override in a subclass.

Registering a Shutdown Hook

For standalone Spring applications (not running in a servlet container), you must explicitly register a JVM shutdown hook so that @PreDestroy callbacks fire when the process exits. The simplest way is to use ConfigurableApplicationContext:

import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class App { public static void main(String[] args) { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); ctx.registerShutdownHook(); // ensures @PreDestroy runs on JVM exit // ... application logic ... } }

Spring Boot does this automatically. In a plain Spring context you are responsible for calling registerShutdownHook() or calling ctx.close() explicitly.

Practical Pattern: Warming Up a Cache

A classic @PostConstruct use case is pre-loading data so the first real request does not pay a cold-start penalty:

import jakarta.annotation.PostConstruct; import org.springframework.stereotype.Service; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @Service public class ProductCatalog { private final ProductRepository repo; private final ConcurrentHashMap<Long, Product> cache = new ConcurrentHashMap<>(); public ProductCatalog(ProductRepository repo) { this.repo = repo; } @PostConstruct void warmUp() { List<Product> featured = repo.findFeatured(); featured.forEach(p -> cache.put(p.getId(), p)); System.out.printf("Warmed up %d featured products%n", featured.size()); } public Product find(long id) { return cache.computeIfAbsent(id, repo::findById); } }

Summary

@PostConstruct and @PreDestroy are the standard, portable lifecycle hooks for Spring beans. They give you safe, well-timed access to run initialisation after injection and cleanup before destruction. The InitializingBean / DisposableBean interfaces predate the annotations and are still valid but couple your bean to the Spring API. For third-party classes you cannot annotate, use the initMethod / destroyMethod attributes of @Bean. Always register a shutdown hook in standalone apps. In the next lesson you will see how lazy initialisation lets you defer this entire startup sequence until a bean is actually needed.