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:
| Phase | Hook 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
| Pattern | When to use |
|---|---|
| @PostConstruct / @PreDestroy | First choice for typical application code. Simple and readable. |
| InitializingBean / DisposableBean | Spring 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.