What do PostConstruct and PreDestroy methods do
Methods annotated with @PostConstruct or @PreDestroy must strictly adhere to the Jakarta Annotations specification: 4. Non-Static: The method must not be static. 5. Exceptions:...
🟢 Junior Level
1. Core Purpose and Lifecycle Role
@PostConstruct and @PreDestroy are standardized lifecycle annotations defined by the JSR-250 / Jakarta EE specification (jakarta.annotation.*). They allow Spring-managed beans to execute initialization logic immediately after dependency wiring and cleanup logic immediately before container shutdown:
@PostConstruct: Identifies a method invoked automatically by the Spring container exactly once after the bean has been instantiated and all dependencies (@Autowired,@Value) have been populated.@PreDestroy: Identifies a method invoked automatically by the container exactly once immediately before the bean is destroyed during graceful container shutdown (ApplicationContext.close()).
[!IMPORTANT] Jakarta EE Standard in Spring Boot 3+ (Spring Framework 6+): The package changed from
javax.annotation.*tojakarta.annotation.*.
2. Concrete Code Example
package com.example.service;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;
@Service
public class RedisCacheService {
private final RedisClient redisClient;
// 1. Instantiation: Constructor creates instance
public RedisCacheService(RedisClient redisClient) {
this.redisClient = redisClient;
System.out.println("1. Constructor: Object created");
}
// 2. Initialization: Called after dependencies are injected
@PostConstruct
public void init() {
System.out.println("2. @PostConstruct: Validating connection & pre-warming cache");
redisClient.connect();
redisClient.preWarm();
}
public String get(String key) {
return redisClient.get(key);
}
// 3. Destruction: Called before container shuts down
@PreDestroy
public void cleanup() {
System.out.println("3. @PreDestroy: Flushing buffers & closing connection pool");
redisClient.flush();
redisClient.disconnect();
}
}
3. Primary Use Cases
| Annotation | Execution Moment | Typical Responsibilities |
|---|---|---|
@PostConstruct |
After constructor completes and all fields/setters are populated | Validating mandatory configuration, pre-warming in-memory caches, registering background workers |
@PreDestroy |
During graceful application shutdown | Closing database connection pools, shutting down thread executors, flushing unwritten disk buffers |
🟡 Middle Level
1. Method Signature Rules (Jakarta Specification)
Methods annotated with @PostConstruct or @PreDestroy must strictly adhere to the Jakarta Annotations specification:
- Zero Parameters: The method must not accept any arguments (
void methodName()). - Return Type: Must be
void(any returned value is silently ignored by the container). - Access Modifiers: May use any access modifier (
public,protected,package-private,private). - Non-Static: The method must not be
static. - Exceptions: May declare and throw both checked and unchecked exceptions (throwing an exception in
@PostConstructhalts startup).
2. Why Use @PostConstruct Instead of Class Constructors?
- Dependency Availability: When using Field Injection (
@Autowired private Repo repo;) or Setter Injection, dependencies are stillnullinside the constructor. In@PostConstruct, all collaborators are guaranteed to be fully wired. - Preventing
thisReference Escape: Executing complex registration or starting asynchronous threads in a constructor can expose an incompletethisreference to other threads before the constructor finishes, violating the Java Memory Model (JMM). - Separation of Concerns: The constructor should strictly allocate the object in JVM heap;
@PostConstructintegrates the bean into the Spring ecosystem.
3. Inheritance Hierarchy Execution Order
When a bean class extends a superclass, lifecycle methods execute in a strict hierarchical order:
public abstract class BaseService {
@PostConstruct
public void baseInit() {
System.out.println("1. Superclass: @PostConstruct");
}
@PreDestroy
public void baseDestroy() {
System.out.println("4. Superclass: @PreDestroy");
}
}
@Service
public class ChildService extends BaseService {
@PostConstruct
public void childInit() {
System.out.println("2. Subclass: @PostConstruct");
}
@PreDestroy
public void childDestroy() {
System.out.println("3. Subclass: @PreDestroy");
}
}
- Initialization Order: Executes top-down (Superclass
@PostConstruct$\to$ Subclass@PostConstruct). - Destruction Order: Executes bottom-up (Subclass
@PreDestroy$\to$ Superclass@PreDestroy). - Overridden Methods: If the child class overrides the parent’s lifecycle method with the exact same signature, the method executes only once following standard Java polymorphism.
4. Lifecycle Differences: Singleton vs Prototype
Singleton Scope:
→ @PostConstruct is invoked ONCE during application startup.
→ @PreDestroy is invoked during ApplicationContext.close().
Prototype Scope:
→ @PostConstruct is invoked EVERY TIME context.getBean() is called.
→ @PreDestroy is NEVER INVOKED BY SPRING! The container surrenders ownership.
🔴 Senior Level
1. Internal Mechanism: InitDestroyAnnotationBeanPostProcessor
Spring handles @PostConstruct and @PreDestroy through a specialized BeanPostProcessor:
CommonAnnotationBeanPostProcessorextendsInitDestroyAnnotationBeanPostProcessor.- During the
postProcessBeforeInitializationphase, it reflects over bean class metadata, identifies methods marked with@PostConstruct, and invokes them viaMethod.invoke(). - During the
postProcessBeforeDestructionphase, it discovers@PreDestroymethods and invokes them before the bean is cleared from memory.
2. The Fatal @PostConstruct AOP Proxy Trap
A prevalent and critical production defect occurs when developers attempt to call transactional or asynchronous methods inside @PostConstruct:
@Service
public class BrokenBootstrapService {
@PostConstruct
public void init() {
// ❌ SILENT CATASTROPHE: No database transaction is opened!
// Calling this.loadInitialData() runs on the raw unproxied instance!
loadInitialData();
}
@Transactional
public void loadInitialData() {
// Database operations execute WITHOUT transaction boundaries!
}
}
Why This Fails:
Lifecycle Sequence:
1. Instantiation (Raw instance allocated in heap)
2. Property Population (Dependencies wired)
3. postProcessBeforeInitialization ──► @PostConstruct runs on raw `this`!
4. postProcessAfterInitialization ──► AOP Dynamic Proxy wraps target bean!
When @PostConstruct executes, the AOP Dynamic Proxy does not exist yet. The internal call runs directly against the raw Java target instance, completely bypassing the TransactionInterceptor.
The Production-Grade Solution:
Listen for ApplicationReadyEvent or ContextRefreshedEvent, which fires after all beans and AOP dynamic proxies are fully initialized:
@Service
public class CorrectBootstrapService {
@EventListener(ApplicationReadyEvent.class) // ✅ Runs AFTER all AOP proxies are active!
@Transactional
public void onApplicationReady() {
loadInitialData(); // Fully protected by active database transaction!
}
public void loadInitialData() { ... }
}
3. Fail-Fast Strategy on @PostConstruct Exceptions
If an unhandled exception is thrown inside a @PostConstruct method:
- Spring catches the exception and wraps it in a
BeanCreationException/BeanInitializationException. - The
ApplicationContextstartup is immediately aborted (Fail-Fast). - The embedded web server (Tomcat/Jetty) is stopped, and the JVM terminates with an exit code error.
[!TIP] Production Best Practice: Leverage
@PostConstructfor mandatory environment validation (e.g., verifying that critical third-party API keys are set, or that mandatory database migrations have run). If a prerequisite is missing, throw an exception immediately to prevent the service from starting and receiving user traffic.
4. When @PreDestroy is GUARANTEED NOT to Execute
Many developers assume that @PreDestroy is always invoked upon application termination. In reality, @PreDestroy will not run in the following circumstances:
- Prototype Scoped Beans: Spring drops all references to prototype beans after initialization; cleanup is strictly the caller’s responsibility.
- Forceful OS Termination (
SIGKILL): Akill -9 <PID>signal in Linux or killing the process via Task Manager in Windows terminates the OS process instantly without giving the JVM a chance to run shutdown hooks. System.exit()without Shutdown Hook: Ifcontext.registerShutdownHook()was not registered.- Fatal JVM Errors: Unrecoverable JVM crashes, segmentation faults, or
OutOfMemoryErrorconditions. - Thread Deadlock in another
@PreDestroy: If an earlier@PreDestroymethod deadlocks or blocks indefinitely, subsequent destruction callbacks may be aborted when the graceful shutdown timeout expires.
5. Graceful Shutdown Mechanics in Spring Boot
When an application receives a standard termination signal (SIGTERM / kill -15):
- The JVM invokes registered Shutdown Hooks.
- Spring Boot stops accepting new incoming HTTP requests at the embedded web server.
- The container waits for in-flight requests to complete up to the graceful shutdown timeout (
spring.lifecycle.timeout-per-shutdown-phase=30s). SmartLifecycle.stop()methods are executed in phase order.- For each singleton bean, Spring sequentially executes:
- Methods annotated with
@PreDestroy DisposableBean.destroy()- Custom
destroyMethodconfigurations.
- Methods annotated with
4 Tricky Questions
1. What are the strict method signature rules for @PostConstruct and @PreDestroy under the Jakarta Annotations specification?
Answer: Under the Jakarta Annotations specification:
- The method must take zero parameters (
void methodName()). - The return type must be
void(any returned object is discarded). - The method cannot be
static. - The method can have any access modifier (
public,protected,package-private,private). - The method is permitted to throw checked or unchecked exceptions. If an exception is thrown in
@PostConstruct, Spring aborts container startup immediately with aBeanCreationException.
2. In what order do @PostConstruct and @PreDestroy execute in an inheritance hierarchy, and what happens if a method is overridden?
Answer:
- Initialization: Executes top-down. The superclass
@PostConstructmethod runs first, followed by the subclass@PostConstructmethod. - Destruction: Executes bottom-up. The subclass
@PreDestroymethod runs first, followed by the superclass@PreDestroymethod. - Overridden Method: If the subclass overrides a superclass lifecycle method with the exact same method signature, the method executes only once (invoking the subclass implementation according to standard Java dynamic dispatch rules).
3. Why does calling an @Transactional or @Async method on this inside @PostConstruct silently fail to apply transactions or thread dispatch?
Answer:
AOP dynamic proxies (which implement @Transactional via TransactionInterceptor and @Async via AsyncExecutionInterceptor) are generated during the final initialization phase: BeanPostProcessor.postProcessAfterInitialization().
The @PostConstruct callback executes earlier, during BeanPostProcessor.postProcessBeforeInitialization(). Inside @PostConstruct, this references the raw, unproxied target instance in the heap. Method calls on this execute directly against raw bytecode, completely bypassing proxy interceptors.
Remedy: Move transactional initialization to an @EventListener(ApplicationReadyEvent.class) listener or execute it programmatically via TransactionTemplate.
4. Under what operational conditions is a method marked with @PreDestroy guaranteed NOT to be invoked?
Answer:
- Prototype Beans: Spring never invokes destruction hooks on prototype-scoped beans.
- Immediate OS Process Termination:
kill -9(SIGKILL), hardware power cuts, or terminating the process via OS task managers. - Fatal JVM Errors: Unhandled
OutOfMemoryErroror JVM core dumps (segfaults). - Missing Shutdown Hook: Applications invoking
System.exit()where Spring’sregisterShutdownHook()was not registered. - Timeout during Graceful Shutdown: If another bean’s
@PreDestroyhangs indefinitely, the container forcefully kills the JVM afterspring.lifecycle.timeout-per-shutdown-phaseexpires.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
”
@PostConstructand@PreDestroyare standardized lifecycle annotations fromjakarta.annotation:
@PostConstructexecutes once after the constructor finishes and all dependencies are injected, but before AOP dynamic proxies are generated. It is used for post-injection validation and cache warming.@PreDestroyexecutes once before bean destruction during graceful context shutdown to release sockets, pools, and buffers.Both methods must take zero arguments and return
void.@PreDestroyis never invoked for prototype-scoped beans, and@Transactionaldoes not work inside@PostConstructbecause AOP proxies are created after@PostConstructfinishes.”
Red Flags & Pitfalls (DO NOT Say)
- ❌ “I can do the exact same thing in the constructor as in
@PostConstruct.” (In constructors, field-injected dependencies arenull, and publishingthiscreates JMM race conditions). - ❌ “Spring guarantees that
@PreDestroyis always called when an application terminates.” (It is never called onSIGKILL, JVM crashes, or for prototype beans). - ❌ “A
@PostConstructmethod can accept arguments if they are annotated with@Autowired.” (Violation of JSR-250: lifecycle methods must take zero arguments). - ❌ “Calling
@Transactionalmethods from@PostConstructopens a database transaction.” (AOP proxies are created after@PostConstruct; the transaction will not open).
Lifecycle Precedence & Hierarchy Summary
Lifecycle Execution Hierarchy:
┌───────────────────────┬───────────────────────────────────────────┬────────────────────────────────────────┐
│ Phase │ Method / Callback │ Invocation Order │
├───────────────────────┼───────────────────────────────────────────┼────────────────────────────────────────┤
│ Initialization (Init) │ Superclass @PostConstruct │ 1st (Top-Down) │
│ │ Subclass @PostConstruct │ 2nd │
│ │ InitializingBean.afterPropertiesSet() │ 3rd │
│ │ Custom @Bean(initMethod = "…") │ 4th │
│ │ ⭐ AOP Dynamic Proxy Generated │ End of Initialization Phase │
├───────────────────────┼───────────────────────────────────────────┼────────────────────────────────────────┤
│ Destruction (Cleanup) │ Subclass @PreDestroy │ 1st (Bottom-Up) │
│ │ Superclass @PreDestroy │ 2nd │
│ │ DisposableBean.destroy() │ 3rd │
│ │ Custom @Bean(destroyMethod = "…") │ 4th │
└───────────────────────┴───────────────────────────────────────────┴────────────────────────────────────────┘