🍃 Section 5 · Question #9

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.* to jakarta.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:

  1. Zero Parameters: The method must not accept any arguments (void methodName()).
  2. Return Type: Must be void (any returned value is silently ignored by the container).
  3. Access Modifiers: May use any access modifier (public, protected, package-private, private).
  4. Non-Static: The method must not be static.
  5. Exceptions: May declare and throw both checked and unchecked exceptions (throwing an exception in @PostConstruct halts startup).

2. Why Use @PostConstruct Instead of Class Constructors?

  1. Dependency Availability: When using Field Injection (@Autowired private Repo repo;) or Setter Injection, dependencies are still null inside the constructor. In @PostConstruct, all collaborators are guaranteed to be fully wired.
  2. Preventing this Reference Escape: Executing complex registration or starting asynchronous threads in a constructor can expose an incomplete this reference to other threads before the constructor finishes, violating the Java Memory Model (JMM).
  3. Separation of Concerns: The constructor should strictly allocate the object in JVM heap; @PostConstruct integrates 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:

  • CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBeanPostProcessor.
  • During the postProcessBeforeInitialization phase, it reflects over bean class metadata, identifies methods marked with @PostConstruct, and invokes them via Method.invoke().
  • During the postProcessBeforeDestruction phase, it discovers @PreDestroy methods 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:

  1. Spring catches the exception and wraps it in a BeanCreationException / BeanInitializationException.
  2. The ApplicationContext startup is immediately aborted (Fail-Fast).
  3. The embedded web server (Tomcat/Jetty) is stopped, and the JVM terminates with an exit code error.

[!TIP] Production Best Practice: Leverage @PostConstruct for 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:

  1. Prototype Scoped Beans: Spring drops all references to prototype beans after initialization; cleanup is strictly the caller’s responsibility.
  2. Forceful OS Termination (SIGKILL): A kill -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.
  3. System.exit() without Shutdown Hook: If context.registerShutdownHook() was not registered.
  4. Fatal JVM Errors: Unrecoverable JVM crashes, segmentation faults, or OutOfMemoryError conditions.
  5. Thread Deadlock in another @PreDestroy: If an earlier @PreDestroy method 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):

  1. The JVM invokes registered Shutdown Hooks.
  2. Spring Boot stops accepting new incoming HTTP requests at the embedded web server.
  3. The container waits for in-flight requests to complete up to the graceful shutdown timeout (spring.lifecycle.timeout-per-shutdown-phase=30s).
  4. SmartLifecycle.stop() methods are executed in phase order.
  5. For each singleton bean, Spring sequentially executes:
    • Methods annotated with @PreDestroy
    • DisposableBean.destroy()
    • Custom destroyMethod configurations.

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:

  1. The method must take zero parameters (void methodName()).
  2. The return type must be void (any returned object is discarded).
  3. The method cannot be static.
  4. The method can have any access modifier (public, protected, package-private, private).
  5. The method is permitted to throw checked or unchecked exceptions. If an exception is thrown in @PostConstruct, Spring aborts container startup immediately with a BeanCreationException.

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 @PostConstruct method runs first, followed by the subclass @PostConstruct method.
  • Destruction: Executes bottom-up. The subclass @PreDestroy method runs first, followed by the superclass @PreDestroy method.
  • 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:

  1. Prototype Beans: Spring never invokes destruction hooks on prototype-scoped beans.
  2. Immediate OS Process Termination: kill -9 (SIGKILL), hardware power cuts, or terminating the process via OS task managers.
  3. Fatal JVM Errors: Unhandled OutOfMemoryError or JVM core dumps (segfaults).
  4. Missing Shutdown Hook: Applications invoking System.exit() where Spring’s registerShutdownHook() was not registered.
  5. Timeout during Graceful Shutdown: If another bean’s @PreDestroy hangs indefinitely, the container forcefully kills the JVM after spring.lifecycle.timeout-per-shutdown-phase expires.

🎯 Interview Cheat Sheet

30-Second Elevator Pitch

”@PostConstruct and @PreDestroy are standardized lifecycle annotations from jakarta.annotation:

  • @PostConstruct executes 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.
  • @PreDestroy executes once before bean destruction during graceful context shutdown to release sockets, pools, and buffers.

Both methods must take zero arguments and return void. @PreDestroy is never invoked for prototype-scoped beans, and @Transactional does not work inside @PostConstruct because AOP proxies are created after @PostConstruct finishes.”


Red Flags & Pitfalls (DO NOT Say)

  • ❌ “I can do the exact same thing in the constructor as in @PostConstruct.” (In constructors, field-injected dependencies are null, and publishing this creates JMM race conditions).
  • ❌ “Spring guarantees that @PreDestroy is always called when an application terminates.” (It is never called on SIGKILL, JVM crashes, or for prototype beans).
  • ❌ “A @PostConstruct method can accept arguments if they are annotated with @Autowired.” (Violation of JSR-250: lifecycle methods must take zero arguments).
  • ❌ “Calling @Transactional methods from @PostConstruct opens 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                                    │
└───────────────────────┴───────────────────────────────────────────┴────────────────────────────────────────┘