Spring - Bean Lifecycle

Every Spring bean goes through a well-defined lifecycle: instantiation, dependency injection, initialisation, active use, and destruction. Spring provides hooks at the initialisation and destruction phases so you can run custom logic, for example, opening a connection pool on startup or releasing resources on shutdown.

Contents

Bean Lifecycle Phases

  1. Instantiation: Spring creates the bean instance.
  2. Property Population: dependencies are injected.
  3. Initialisation: @PostConstruct methods are called.
  4. Active use: the bean serves the application.
  5. Destruction: @PreDestroy methods are called when the context is closed.

@PostConstruct: initialisation hook

A method annotated with @PostConstruct is called by Spring after the bean is fully initialised. Use it for one-time setup such as loading configuration or warming up a cache.

Java
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class CacheWarmupService {

    private final ProductRepository productRepository;
    private List<Product> cachedProducts;

    public CacheWarmupService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @PostConstruct
    public void warmUpCache() {
        cachedProducts = productRepository.findAll();
        System.out.println("Cache warmed up with " + cachedProducts.size() + " products");
    }
}

Rules: the method must be void, take no parameters, and must not be static.

@PreDestroy: destruction hook

A method annotated with @PreDestroy is called by Spring before the bean is removed from the context. Use it to release resources such as thread pools or file handles.

Java
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@Service
public class AsyncTaskService {

    private final ExecutorService executor = Executors.newFixedThreadPool(4);

    public void submitTask(Runnable task) {
        executor.submit(task);
    }

    @PreDestroy
    public void shutdown() throws InterruptedException {
        System.out.println("Shutting down thread pool...");
        executor.shutdown();
        if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
            executor.shutdownNow();
        }
        System.out.println("Thread pool terminated.");
    }
}

InitializingBean and DisposableBean interfaces

An older approach is implementing InitializingBean or DisposableBean. This couples the class to Spring's API and is generally less preferred than the annotation approach.

Java
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

@Component
public class DatabaseConnectionPool implements InitializingBean, DisposableBean {

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("Connection pool initialised");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("Connection pool closed");
    }
}

Prefer @PostConstruct / @PreDestroy: they are part of the Jakarta EE standard and keep your classes Spring-independent.