Once you become comfortable with DI and AutoConfiguration, the next natural question is: “At what point in the lifecycle can I hook into a Bean, and what methods can I call?”

If you need to load master data at startup or close a DB connection at shutdown, Bean lifecycle hooks are the answer. This article organizes how they work and covers four implementation patterns.

Bean Lifecycle Overview

Beans managed by Spring go through five phases:

Instantiation

Dependency Injection (DI)

Initialization callbacks (@PostConstruct, etc.)

In use

Destruction callbacks (@PreDestroy, etc.)

The available hooks for each phase are as follows:

PhaseHook mechanism
Initialization@PostConstruct, InitializingBean#afterPropertiesSet, @Bean(initMethod)
Destruction@PreDestroy, DisposableBean#destroy, @Bean(destroyMethod)

The key point is that initialization callbacks are invoked after DI is complete. Since injected fields are not yet available inside the constructor, startup logic should be written in @PostConstruct.

Writing Initialization Logic with @PostConstruct

import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;

@Component
public class GreetingService {

    private final MessageRepository messageRepository;

    public GreetingService(MessageRepository messageRepository) {
        this.messageRepository = messageRepository;
    }

    @PostConstruct
    public void init() {
        // Called after DI is complete
        String msg = messageRepository.findDefaultMessage();
        System.out.println("Initialization complete: " + msg);
    }
}

In Spring Boot 3.x and later, the import path is jakarta.annotation.PostConstruct. In the 2.x era it was javax.annotation.PostConstruct, but this changed with the migration to Jakarta EE. Be careful when referencing older sample code.

Writing Shutdown Logic with @PreDestroy

This is called when the ApplicationContext is closed, including during JVM shutdown.

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class ConnectionHolder {

    private static final Logger log = LoggerFactory.getLogger(ConnectionHolder.class);

    private SomeExternalConnection connection;

    @PostConstruct
    public void open() {
        connection = new SomeExternalConnection();
        connection.connect();
    }

    @PreDestroy
    public void close() {
        try {
            connection.close();
        } catch (Exception e) {
            log.warn("Failed to close connection", e);
        }
    }
}

Note that @PreDestroy is not called for prototype-scoped Beans. Spring removes prototype instances from management after creation, so the destruction hook does not fire. If resource cleanup is needed, you must manage it manually — for example, by implementing DisposableBean and explicitly calling ((DisposableBean) bean).destroy() from the caller.

Using the InitializingBean and DisposableBean Interfaces

You can also implement interfaces instead of using annotations.

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

@Component
public class LegacyService implements InitializingBean, DisposableBean {

    @Override
    public void afterPropertiesSet() throws Exception {
        // Called right after @PostConstruct
        System.out.println("Initialization logic");
    }

    @Override
    public void destroy() throws Exception {
        // Called right after @PreDestroy
        System.out.println("Destruction logic");
    }
}

Both @PostConstruct and afterPropertiesSet() are called during the initialization phase, but Spring processes @PostConstruct first via CommonAnnotationBeanPostProcessor, then calls afterPropertiesSet(). The execution order is @PostConstruct → afterPropertiesSet(). The same applies at destruction: @PreDestroy → destroy().

Because this approach creates a strong dependency on the Spring API, it is more commonly used when developing Spring libraries or framework internals rather than in application business code.

Specifying Hooks via Java Config with @Bean(initMethod / destroyMethod)

This is useful when you need to attach lifecycle hooks to a third-party class whose source you cannot modify.

@Configuration
public class AppConfig {

    @Bean(initMethod = "start", destroyMethod = "stop")
    public SomeThirdPartyService thirdPartyService() {
        return new SomeThirdPartyService();
    }
}

destroyMethod has a default inference mechanism. If not specified explicitly, Spring will automatically treat a close() or shutdown() method on the target class as the destroy method. To disable this auto-detection, set destroyMethod = "".

When to Use Each Pattern

PatternWhen to use
@PostConstruct / @PreDestroyFirst choice for typical application code. Simple and readable.
InitializingBean / DisposableBeanSpring library or framework development. Introduces a Spring dependency.
@Bean(initMethod / destroyMethod)When you need to add hooks to an external class you cannot modify.

When in doubt, go with @PostConstruct / @PreDestroy.

Practical Example: Loading Initial Data at Startup

Loading master data into a cache is a classic use case for @PostConstruct.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class ProductCacheService {

    private static final Logger log = LoggerFactory.getLogger(ProductCacheService.class);

    private final ProductRepository productRepository;
    private final Map<Long, Product> cache = new ConcurrentHashMap<>();

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

    @PostConstruct
    public void loadCache() {
        productRepository.findAll()
            .forEach(p -> cache.put(p.getId(), p));
        log.info("Product master cached: {} entries", cache.size());
    }

    public Product findById(Long id) {
        return cache.get(id);
    }
}

The advantage of using @PostConstruct is that by the time this Bean is injected into another Bean, the cache is already fully populated. On the other hand, if you need to run logic after all Beans have finished initializing, CommandLineRunner or ApplicationRunner is more appropriate. For cases where you need fine-grained control over execution timing — such as logic that depends on the state of other Beans, or warm-up tasks after startup completes — CommandLineRunner is the better fit.

Practical Example: Releasing Resources at Shutdown

For Beans that hold thread pools or external connections, cleanup via @PreDestroy is important.

@Component
public class SchedulerService {

    private ScheduledExecutorService scheduler;

    @PostConstruct
    public void start() {
        scheduler = Executors.newScheduledThreadPool(4);
    }

    @PreDestroy
    public void stop() {
        scheduler.shutdown();
        try {
            if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
                scheduler.shutdownNow();
            }
        } catch (InterruptedException e) {
            scheduler.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

Spring Boot automatically closes the embedded Tomcat, so you do not need to write code to close Tomcat yourself.

Common Pitfalls

@PostConstruct Is Not Being Called

A common cause is that the class is not registered as a Bean. Verify that it has an annotation such as @Component and that it resides in a package covered by component scanning. Also note that attaching @PostConstruct to a static method or a method with parameters will not work. Per the spec, the target method must be void, no-argument, and non-static. If applied to a method with parameters, an IllegalStateException will be thrown at startup.

Controlling Initialization Order Across Multiple Beans

If BeanA’s initialization must happen after BeanB’s completes, you can explicitly declare the dependency with @DependsOn("beanB").

Summary

The Bean lifecycle flows as: Instantiation → DI → Initialization callbacks → In use → Destruction callbacks. For typical application code, @PostConstruct and @PreDestroy cover the vast majority of cases.

For more on how Beans are created, see What is DI and What is @Component. Reading these alongside What is @Configuration and How AutoConfiguration Works will give you a complete picture of how Beans are registered in the ApplicationContext. Combining @Async and @Scheduled with lifecycle hooks is covered in detail in the Spring Boot Async Processing Guide.