Bean Lifecycle

Understand the complete Spring bean lifecycle — from instantiation through initialization to destruction — and how to hook into each phase for initialization and cleanup.

Last reviewed: January 2025

·Spring Boot 2.7Spring Boot 3.xSpring 6
✅
Quick Understanding: Every Spring bean goes through a lifecycle: created (constructor called), dependencies injected, initialized (@PostConstruct), ready for use, then destroyed (@PreDestroy on shutdown). You can hook into any phase. The most important hooks are @PostConstruct (run code after injection) and @PreDestroy (cleanup before shutdown).

🔍 What Is It?

The Spring bean lifecycle is the sequence of events from when a bean is created to when it's destroyed.

Full lifecycle sequence:

  1. Instantiation — constructor called
  2. Dependency Injection — properties set, constructor/setter injection
  3. BeanNameAware.setBeanName() — bean learns its name
  4. BeanFactoryAware.setBeanFactory() — bean gets container reference
  5. ApplicationContextAware.setApplicationContext() — bean gets context
  6. BeanPostProcessor.postProcessBeforeInitialization() — pre-init processing
  7. @PostConstruct method called
  8. InitializingBean.afterPropertiesSet() — programmatic init
  9. @Bean(initMethod="...") — custom init method
  10. BeanPostProcessor.postProcessAfterInitialization() — post-init (AOP proxies created here)
  11. Bean is ready — in use
  12. @PreDestroy method called
  13. DisposableBean.destroy() — programmatic cleanup
  14. @Bean(destroyMethod="...") — custom destroy method

In practice, you only need:

  • →@PostConstruct for initialization
  • →@PreDestroy for cleanup

🎯 Why Do We Need It?

Lifecycle hooks let you:

  • →Initialize resources after all dependencies are injected (cache warmup, connection validation)
  • →Register listeners once the bean is fully set up
  • →Clean up resources gracefully on shutdown (close connections, flush buffers)
  • →Validate configuration at startup rather than at first use

Without lifecycle hooks, you'd have initialization logic in constructors (where dependencies might not be available) or scattered across the codebase.

🏗️ What Happens in Real Projects?

Common lifecycle patterns in enterprise apps:

  • →@PostConstruct: warm up caches, validate external service connectivity, start background threads
  • →@PreDestroy: close database connections, flush message queues, stop schedulers
  • →BeanPostProcessor: AOP proxies are created here (every @Transactional bean goes through this)
  • →ApplicationListener<ContextRefreshedEvent>: run code when ALL beans are initialized

⚙️ How It Works

@PostConstruct:

  • →Runs after constructor AND after all @Autowired injections are complete
  • →Safe to use dependencies here (unlike constructor, where DI may not be done)
  • →Runs once per bean instance
  • →If it throws, the application fails to start

@PreDestroy:

  • →Runs when the ApplicationContext is closing
  • →For web apps: triggered by JVM shutdown hook or explicit close()
  • →Prototype-scoped beans: @PreDestroy is NOT called (Spring doesn't track them)
  • →Runs in reverse order of initialization

BeanPostProcessor:

  • →Runs for EVERY bean in the context
  • →Used by Spring internally for AOP proxying, @Async, @Transactional
  • →Implement BeanPostProcessor to add cross-cutting behavior to all beans

💻 Example

Bean Lifecycle in Practice — Real-world lifecycle hooks for initialization and cleanup.

java
@Service
@Slf4j
public class DatabaseHealthService {

    private final DataSource dataSource;
    private final AppProperties appProperties;
    private ScheduledExecutorService healthChecker;

    public DatabaseHealthService(DataSource dataSource, AppProperties appProperties) {
        this.dataSource = dataSource;
        this.appProperties = appProperties;
        // DON'T do initialization here — appProperties might not be fully bound yet
    }

    @PostConstruct  // All dependencies injected, safe to use them
    public void initialize() {
        log.info("Validating database connectivity...");
        try (Connection conn = dataSource.getConnection()) {
            boolean valid = conn.isValid(5); // 5 second timeout
            if (!valid) throw new IllegalStateException("Database connection invalid");
            log.info("Database connection validated successfully");
        } catch (SQLException e) {
            throw new IllegalStateException("Cannot connect to database at startup", e);
        }

        // Start background health checker
        healthChecker = Executors.newSingleThreadScheduledExecutor();
        healthChecker.scheduleAtFixedRate(
            this::checkHealth, 30, 30, TimeUnit.SECONDS
        );
        log.info("Database health checker started");
    }

    @PreDestroy  // Called when application shuts down
    public void cleanup() {
        log.info("Shutting down database health checker");
        if (healthChecker != null) {
            healthChecker.shutdown();
            try {
                healthChecker.awaitTermination(10, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    private void checkHealth() {
        try (Connection conn = dataSource.getConnection()) {
            conn.isValid(2);
        } catch (SQLException e) {
            log.error("Database health check failed", e);
        }
    }
}

🤔 Common Confusions

❓ Why can't I use dependencies in the constructor like I can in @PostConstruct?

In the constructor, Spring hasn't injected dependencies yet (for setter/field injection). By @PostConstruct time, all injection is complete. With constructor injection, dependencies ARE available in the constructor — but @PostConstruct is still better for initialization because it runs after the full bean setup, including any BeanPostProcessor processing.

❓ Is @PreDestroy called for prototype beans?

No. Spring creates prototype beans on demand but doesn't track them after creation. @PreDestroy is never called for prototype-scoped beans. If you need cleanup for prototypes, implement DisposableBean and call destroy() manually, or use a singleton factory that manages the lifecycle.

❓ What is the difference between ApplicationReadyEvent and ContextRefreshedEvent?

ContextRefreshedEvent fires when the ApplicationContext is refreshed (all beans initialized). ApplicationReadyEvent fires after the context is refreshed AND the application is ready to serve requests (after CommandLineRunner and ApplicationRunner have run). Use ApplicationReadyEvent for post-startup tasks in web applications.

🚨 Common Production Issues

@PostConstruct making blocking network calls that delay startup — run heavy initialization asynchronously or use ApplicationReadyEvent

@PreDestroy not called because JVM killed with SIGKILL instead of SIGTERM — configure Kubernetes terminationGracePeriodSeconds

Circular dependency in BeanPostProcessor — BPPs are special and must not depend on beans they process

Long @PreDestroy blocking graceful shutdown — set spring.lifecycle.timeout-per-shutdown-phase

ApplicationContext refreshed multiple times in tests — use @DirtiesContext carefully

✅ Best Practices

Use @PostConstruct for initialization logic, never in constructors

Keep @PostConstruct fast — slow init delays application startup

Use @PreDestroy to close resources (connections, threads, file handles)

For heavy startup work, use ApplicationReadyEvent so the app starts fast and initializes async

Configure graceful shutdown: server.shutdown=graceful in application.properties

🏛️ Architect's Note

Senior Architect's Perspective

In cloud-native deployments, startup time matters. Kubernetes readiness probes wait for your app to be healthy before routing traffic. If your @PostConstruct methods take 30 seconds loading caches, your pod won't become ready for 30 seconds. Pattern: start fast, serve traffic, warm up asynchronously. Use ApplicationReadyEvent for non-critical initialization and async cache warming. Critical validation (DB connectivity) belongs in @PostConstruct.

💬 Frequently Asked Questions

How do I run code once after the entire application starts?

Implement CommandLineRunner or ApplicationRunner — their run() method is called after the ApplicationContext is fully initialized. Or use @EventListener(ApplicationReadyEvent.class). For one-time database migrations, Flyway/Liquibase run automatically via auto-configuration.

What happens if @PostConstruct throws an exception?

The ApplicationContext fails to start and the application exits. This is intentional — if a bean can't initialize, the app shouldn't run. This is why validating external service connectivity in @PostConstruct is a valid pattern — fail fast at startup rather than failing at first request.

🎯 Key Takeaways

1@PostConstruct runs after all dependencies are injected — safe for initialization
2@PreDestroy runs on application shutdown — use for resource cleanup
3BeanPostProcessor processes every bean — used by Spring for AOP proxying
4ApplicationReadyEvent fires after full startup — use for post-startup tasks
5@PreDestroy is NOT called for prototype-scoped beans
6Configure graceful shutdown with server.shutdown=graceful

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these Spring Boot references to verify the concept in official docs and continue into the most relevant SplashRide pages.

Related SplashRide pages

SplashRide Spring Boot learning path

Browse the full Spring Boot topic collection and learning order on SplashRide.

Spring Boot Interview Prep

Practice Spring Boot interview questions and scenario-based answers.

Backend Engineer Career Path

A structured journey through Core Java, Spring Boot, production backend engineering, and interview preparation.

Full Stack Java Developer Career Path

A guided route through frontend foundations, Java backend engineering, delivery workflows, and interview readiness.

IoC Container

Understand the Spring IoC Container — the heart of Spring that manages object creation, dependency wiring, and bean lifecycle in every enterprise application.

Dependency Injection

Master Spring's three DI styles — constructor, setter, and field injection — and understand why constructor injection is the only correct choice for production code.