HomeSpring BootSpring Boot Bean Lifecycle: Phases, Callbacks and Examples
Spring Boot

Spring Boot Bean Lifecycle: Phases, Callbacks and Examples

Spring bean lifecycle explained step by step: instantiation, injection, @PostConstruct, @PreDestroy, BeanPostProcessor, scopes and lazy init, with Java examples.

Spring Bean Lifecycle in 30 Seconds

The Spring bean lifecycle is the sequence of steps the container performs to create, configure, use and destroy a bean: register definition → instantiate → inject dependencies → run init callbacks → use → run destroy callbacks. Quick reference: 1. Constructor runs (object exists, field/setter injection not done yet) 2. Dependencies are injected 3. Aware callbacks run (BeanNameAware, ApplicationContextAware) 4. @PostConstruct runs 5. InitializingBean.afterPropertiesSet() runs 6. Custom initMethod runs 7. Bean is ready and in use (AOP proxy is created right after init) 8. @PreDestroy runs on shutdown 9. DisposableBean.destroy() runs 10. Custom destroyMethod runs Rule of thumb: use the constructor for wiring, @PostConstruct for startup logic that needs injected dependencies, and @PreDestroy for cleanup. Everything below explains why, with code you can run.

See the Lifecycle Order Yourself

Instead of memorizing the order, run this bean and watch the console. It implements every common hook and logs when each one fires.
Java
import jakarta.annotation.PostConstruct;   // Spring Boot 3+ / Spring 6
import jakarta.annotation.PreDestroy;    // Spring Boot 2.x uses javax.annotation.*

@Component
@Slf4j
public class LifecycleDemoBean implements BeanNameAware, ApplicationContextAware,
        InitializingBean, DisposableBean {

    public LifecycleDemoBean() { log.info("1. Constructor"); }

    @Autowired
    public void setEnvironment(Environment env) { log.info("2. Dependency injection (setter)"); }

    @Override
    public void setBeanName(String name) { log.info("3. BeanNameAware -> {}", name); }

    @Override
    public void setApplicationContext(ApplicationContext ctx) { log.info("3. ApplicationContextAware"); }

    @PostConstruct
    public void postConstruct() { log.info("4. @PostConstruct"); }

    @Override
    public void afterPropertiesSet() { log.info("5. InitializingBean.afterPropertiesSet"); }

    @PreDestroy
    public void preDestroy() { log.info("6. @PreDestroy"); }

    @Override
    public void destroy() { log.info("7. DisposableBean.destroy"); }
}

// Console output at startup/shutdown:
// 1. Constructor
// 2. Dependency injection (setter)
// 3. BeanNameAware -> lifecycleDemoBean
// 3. ApplicationContextAware
// 4. @PostConstruct
// 5. InitializingBean.afterPropertiesSet
// ... application runs ...
// 6. @PreDestroy
// 7. DisposableBean.destroy

The Complete Bean Lifecycle

A Spring bean passes through twelve phases from configuration reading to destruction. Each phase has hooks that let you plug in custom behavior at the right moment. Phase 1 — BeanDefinition creation: Spring reads your annotations and configuration and creates a BeanDefinition for each bean: metadata about how to create it, its scope and its dependencies. Phase 2 — BeanFactoryPostProcessor: These run against BeanDefinitions before any bean is instantiated. PropertySourcesPlaceholderConfigurer, which resolves ${...} placeholders, runs here. Phase 3 — Instantiation: Spring calls the constructor. Constructor-injected dependencies are available; field and setter injection has not happened yet. Phase 4 — Dependency injection: Spring injects @Autowired fields and setters and performs @Value injection. This work is itself done by a built-in BeanPostProcessor (AutowiredAnnotationBeanPostProcessor). Phase 5 — Aware callbacks and BeanPostProcessor (before init): BeanNameAware, BeanFactoryAware and ApplicationContextAware callbacks run, then every BeanPostProcessor's postProcessBeforeInitialization. The processor that handles @PostConstruct (CommonAnnotationBeanPostProcessor) runs in this step. Phase 6 — @PostConstruct: Your initialization method runs. All dependencies are injected and available. Phase 7 — InitializingBean.afterPropertiesSet(): Called if your bean implements InitializingBean. Phase 8 — Custom init-method: Called if declared via @Bean(initMethod = "..."). Phase 9 — BeanPostProcessor (after init): postProcessAfterInitialization runs. AOP proxies (@Transactional, @Cacheable, @Async) are created here, so the object other beans receive may be a proxy wrapping your bean. Phase 10 — Bean in use: The bean is registered in the context and injectable everywhere. Phase 11 — @PreDestroy: On shutdown, your cleanup method runs first. Phase 12 — DisposableBean.destroy() and custom destroyMethod: Called after @PreDestroy.

Initialization Hooks — Three Ways

Spring provides three mechanisms for running code after a bean's dependencies are injected and before it's made available to the rest of the application. They run in order: @PostConstruct, then InitializingBean, then custom init-method.
Java
// WAY 1 — @PostConstruct (RECOMMENDED):
@Component
public class DatabaseConnectionPool {

    private final DataSourceProperties properties;
    private HikariDataSource pool;

    public DatabaseConnectionPool(DataSourceProperties properties) {
        this.properties = properties;
        // Don't initialize here — dependencies may not be fully ready
    }

    @PostConstruct
    public void initialize() {
        // Called AFTER constructor AND after all @Autowired injection
        // All dependencies are guaranteed to be set and ready
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(properties.getUrl());
        config.setUsername(properties.getUsername());
        config.setPassword(properties.getPassword());
        config.setMaximumPoolSize(10);
        this.pool = new HikariDataSource(config);
        System.out.println("Connection pool initialized: " + pool.getPoolName());
    }
}

// WAY 2 — InitializingBean interface:
@Component
public class CacheWarmer implements InitializingBean {

    private final ProductRepository productRepository;
    private final CacheManager cacheManager;

    public CacheWarmer(ProductRepository productRepository, CacheManager cacheManager) {
        this.productRepository = productRepository;
        this.cacheManager = cacheManager;
    }

    @Override
    public void afterPropertiesSet() {
        // Same timing as @PostConstruct — called after all injection
        List<Product> topProducts = productRepository.findTop100ByOrderBySalesDesc();
        Cache cache = cacheManager.getCache("products");
        topProducts.forEach(p -> cache.put(p.getId(), p));
        System.out.println("Cache warmed with " + topProducts.size() + " products");
    }
}

// WAY 3 — Custom init-method declared on @Bean:
@Configuration
public class InfrastructureConfig {

    @Bean(initMethod = "start")
    public SchedulerService schedulerService() {
        return new SchedulerService();
    }
}

public class SchedulerService {
    public void start() {
        // Called by Spring after injection — equivalent to @PostConstruct
        System.out.println("Scheduler started");
    }
}

// ORDER when all three are present on the same bean:
// 1. @PostConstruct
// 2. InitializingBean.afterPropertiesSet()
// 3. Custom init-method

Destruction Hooks — Three Ways

Destruction hooks run when the Spring context shuts down — on graceful application shutdown (SIGTERM, Ctrl+C, context.close()). They are the right place to release resources, flush buffers, and close connections.
Java
// WAY 1 — @PreDestroy (RECOMMENDED):
@Component
public class MessageQueueConsumer {

    private volatile boolean running = false;
    private ExecutorService executor;

    @PostConstruct
    public void start() {
        running = true;
        executor = Executors.newFixedThreadPool(5);
        executor.submit(this::consumeMessages);
        System.out.println("Message queue consumer started");
    }

    @PreDestroy
    public void stop() {
        // Called when ApplicationContext is closing
        // Spring Boot graceful shutdown waits for this before JVM exits
        running = false;
        executor.shutdown();
        try {
            if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }
        System.out.println("Message queue consumer stopped — drained cleanly");
    }

    private void consumeMessages() {
        while (running) {
            // process messages...
        }
    }
}

// WAY 2 — DisposableBean interface:
@Component
public class SearchIndexManager implements DisposableBean {

    private final ElasticsearchClient client;

    public SearchIndexManager(ElasticsearchClient client) {
        this.client = client;
    }

    @Override
    public void destroy() {
        // Called after @PreDestroy — flush pending index operations
        client.indices().flush();
        client.close();
        System.out.println("Search index flushed and client closed");
    }
}

// WAY 3 — Custom destroyMethod declared on @Bean:
@Configuration
public class InfrastructureConfig {

    @Bean(destroyMethod = "shutdown")
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.initialize();
        return executor;
    }
    // Spring calls executor.shutdown() on context close automatically
}

// ORDER when all three are present on the same bean:
// 1. @PreDestroy
// 2. DisposableBean.destroy()
// 3. Custom destroyMethod

// IMPORTANT: @PreDestroy is only called for singleton-scoped beans.
// Prototype-scoped beans are NOT destroyed by Spring —
// you are responsible for releasing their resources.

BeanPostProcessor — Intercepting Every Bean

BeanPostProcessor is Spring's most powerful extension point. It intercepts every bean in the container before and after initialization — used by Spring itself to implement @Autowired processing, AOP proxy creation, validation, and more.
Java
// BeanPostProcessor interface — intercepts every bean:
public interface BeanPostProcessor {
    // Called BEFORE @PostConstruct:
    Object postProcessBeforeInitialization(Object bean, String beanName);

    // Called AFTER @PostConstruct (where AOP proxies are created):
    Object postProcessAfterInitialization(Object bean, String beanName);
}

// Custom BeanPostProcessor — example: measure initialization time of every bean:
@Component
public class InitializationTimingPostProcessor implements BeanPostProcessor {

    private final Map<String, Long> startTimes = new ConcurrentHashMap<>();

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        startTimes.put(beanName, System.currentTimeMillis());
        return bean;   // return the bean unchanged (or return a modified version)
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        Long startTime = startTimes.remove(beanName);
        if (startTime != null) {
            long elapsed = System.currentTimeMillis() - startTime;
            if (elapsed > 100) {   // only log slow initializations
                System.out.printf("Slow bean init: %s took %dms%n", beanName, elapsed);
            }
        }
        return bean;
    }
}

// Built-in BeanPostProcessors Spring uses internally:
// AutowiredAnnotationBeanPostProcessor   — processes @Autowired, @Value, @Inject
// CommonAnnotationBeanPostProcessor      — processes @PostConstruct, @PreDestroy, @Resource
// PersistenceAnnotationBeanPostProcessor — processes @PersistenceContext, @PersistenceUnit
// AbstractAdvisorAutoProxyCreator        — creates AOP proxies for @Transactional, @Cacheable

// BeanFactoryPostProcessor — runs against BeanDefinitions BEFORE instantiation:
@Component
public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        // Modify BeanDefinitions before any beans are created
        BeanDefinition userServiceDef = beanFactory.getBeanDefinition("userService");
        userServiceDef.setScope(BeanDefinition.SCOPE_PROTOTYPE);   // change scope
        System.out.println("Modified userService scope to prototype");
    }
}

Bean Scopes and Lifecycle

A bean's scope determines how many instances are created and how long they live. Each scope has different lifecycle behavior — especially around destruction callbacks.
Java
// SINGLETON (default) — one instance for the entire ApplicationContext:
@Component
// or @Scope("singleton") — explicit but redundant
public class UserService {
    // Created once at startup (or first use if @Lazy)
    // @PostConstruct called once
    // @PreDestroy called once on shutdown
    // Shared by every bean that injects UserService
}

// PROTOTYPE — new instance every time it's requested:
@Component
@Scope("prototype")
public class ReportGenerator {
    // New instance created every time @Autowired or context.getBean() is called
    // @PostConstruct called on each new instance
    // @PreDestroy NOT called — you are responsible for cleanup
    // Never shared — each caller gets their own instance
}

// REQUEST — one instance per HTTP request (web apps only):
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
       proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private String requestId = UUID.randomUUID().toString();
    private long startTime = System.currentTimeMillis();
    // Created at request start, destroyed at request end
    // proxyMode required when injecting into singleton beans
}

// SESSION — one instance per HTTP session:
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,
       proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSessionData {
    private String userId;
    private List<String> recentSearches = new ArrayList<>();
    // Created when session starts, destroyed when session expires/invalidates
}

// Injecting a prototype into a singleton — the problem:
@Service  // singleton
public class OrderProcessor {

    @Autowired
    private ReportGenerator reportGenerator;  // prototype — but only injected ONCE
    // Every call to orderProcessor.process() uses the SAME ReportGenerator instance
    // The singleton holds it — the prototype behavior is effectively lost

    // FIX — look up a new prototype instance each time:
    @Autowired
    private ApplicationContext context;

    public void process(Order order) {
        ReportGenerator generator = context.getBean(ReportGenerator.class); // fresh instance
        generator.generate(order);
    }
}

Lifecycle Events and Listeners

Spring publishes ApplicationEvents at key points in the container lifecycle. The startup order in Spring Boot is: 1. ContextRefreshedEvent: all singleton beans are created 2. ApplicationRunner / CommandLineRunner beans execute 3. ApplicationReadyEvent: the app is ready to serve traffic On shutdown, ContextClosedEvent is published before any @PreDestroy method runs.
Java
@Component
@Slf4j
public class ApplicationLifecycleListener {

    @EventListener(ContextRefreshedEvent.class)
    public void onContextRefreshed(ContextRefreshedEvent event) {
        log.info("Context refreshed - all singleton beans created");
    }

    // Published AFTER all runners have finished:
    @EventListener(ApplicationReadyEvent.class)
    public void onApplicationReady(ApplicationReadyEvent event) {
        log.info("Application ready - safe to accept traffic, start schedulers");
    }

    // Published BEFORE @PreDestroy methods:
    @EventListener(ContextClosedEvent.class)
    public void onContextClosing(ContextClosedEvent event) {
        log.info("Context closing - flush pending operations");
    }
}

// ApplicationRunner - runs after context refresh, BEFORE ApplicationReadyEvent:
@Component
@Order(1)   // control execution order when multiple runners exist
public class DataMigrationRunner implements ApplicationRunner {

    private final MigrationService migrationService;

    public DataMigrationRunner(MigrationService migrationService) {
        this.migrationService = migrationService;
    }

    @Override
    public void run(ApplicationArguments args) {
        migrationService.migrate();
    }
}

// CommandLineRunner - same timing, receives raw String[] args:
@Component
@Order(2)   // runs after DataMigrationRunner
public class CacheWarmupRunner implements CommandLineRunner {

    private final CacheService cacheService;

    public CacheWarmupRunner(CacheService cacheService) {
        this.cacheService = cacheService;
    }

    @Override
    public void run(String... args) {
        cacheService.warmAll();
    }
}

// SmartLifecycle - fine-grained control over start/stop order:
@Component
public class BackgroundJobScheduler implements SmartLifecycle {

    private volatile boolean running = false;

    @Override public void start() { running = true; /* start threads */ }
    @Override public void stop()  { running = false; /* stop gracefully */ }
    @Override public boolean isRunning() { return running; }
    @Override public int getPhase() { return 0; } // lower phase starts first, stops last
}

Lazy Initialization

By default, Spring creates all singleton beans at startup — eager initialization. Lazy initialization delays bean creation until the first time the bean is requested, reducing startup time at the cost of slower first requests.
Java
// @Lazy on a single bean — created on first use:
@Service
@Lazy
public class ReportingService {
    // NOT created at startup
    // Created the first time something requests or injects ReportingService
    // Startup is faster — first request that needs ReportingService is slower
}

// @Lazy on an injection point — lazy proxy injected:
@Service
public class UserService {

    @Autowired
    @Lazy
    private ReportingService reportingService;
    // UserService is created eagerly at startup
    // A proxy is injected for reportingService
    // ReportingService itself is created on the first call to reportingService.xxx()
}

// Global lazy initialization — ALL beans lazy (Spring Boot 2.2+):
// application.properties:
// spring.main.lazy-initialization=true

// When to use lazy initialization:
// - Large applications with many beans where startup time matters
// - Beans that are rarely used (admin tools, reporting services)
// - Development environments where you want faster restarts

// When NOT to use lazy initialization:
// - Production critical path beans (startup errors surface at first request, not startup)
// - Beans with @PostConstruct that must run before the app accepts traffic
// - Database migration (Flyway) — must run before any DB access

// @Lazy in @Configuration:
@Configuration
public class AppConfig {

    @Bean
    @Lazy
    public ExpensiveService expensiveService() {
        // Created only when first requested — not at startup
        return new ExpensiveService();
    }
}

// Smart strategy — lazy globally, eager for critical beans:
// spring.main.lazy-initialization=true (in application.properties)

@Component
@Lazy(false)   // override global lazy — always eager:
public class DatabaseHealthChecker {
    @PostConstruct
    public void checkDatabase() {
        // This MUST run at startup — mark as eager
    }
}

Lifecycle in Practice — Complete Example

A realistic bean combining constructor injection, validation in @PostConstruct, a background worker started after the app is ready, and a clean shutdown. Note that KafkaConsumer is not thread-safe: it must be polled and closed from the same thread, and wakeup() is the only method safe to call from another thread.
Java
@Service
@Slf4j
public class KafkaConsumerService {

    private final KafkaProperties kafkaProperties;
    private final OrderService orderService;
    private volatile KafkaConsumer<String, String> consumer;
    private ExecutorService executorService;
    private volatile boolean consuming = false;

    // Constructor-injected dependencies are available here
    public KafkaConsumerService(KafkaProperties kafkaProperties, OrderService orderService) {
        this.kafkaProperties = kafkaProperties;
        this.orderService = orderService;
    }

    // Lightweight validation only - fail fast at startup
    @PostConstruct
    public void validate() {
        if (kafkaProperties.getBootstrapServers() == null) {
            throw new IllegalStateException("Kafka bootstrap servers not configured");
        }
    }

    // Called by KafkaStartupRunner after the context is ready
    public void startConsuming() {
        consuming = true;
        executorService = Executors.newSingleThreadExecutor();
        executorService.submit(() -> {
            // Create, poll and close the consumer in the SAME thread
            Properties props = new Properties();
            props.put("bootstrap.servers", kafkaProperties.getBootstrapServers());
            props.put("group.id", kafkaProperties.getGroupId());
            props.put("key.deserializer", StringDeserializer.class.getName());
            props.put("value.deserializer", StringDeserializer.class.getName());
            props.put("enable.auto.commit", "false");

            consumer = new KafkaConsumer<>(props);
            try (consumer) {
                consumer.subscribe(List.of("orders"));
                while (consuming) {
                    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
                    records.forEach(r -> orderService.processOrderEvent(r.value()));
                    if (!records.isEmpty()) consumer.commitSync();
                }
            } catch (WakeupException e) {
                // expected on shutdown
            }
            log.info("Kafka consumer loop exited");
        });
    }

    @PreDestroy
    public void shutdown() throws InterruptedException {
        consuming = false;
        if (consumer != null) consumer.wakeup();   // only thread-safe consumer method
        if (executorService != null) {
            executorService.shutdown();
            executorService.awaitTermination(10, TimeUnit.SECONDS);
        }
        log.info("Kafka consumer cleanly shut down");
    }
}

@Component
@Order(Ordered.LOWEST_PRECEDENCE)   // run last, after all other runners
public class KafkaStartupRunner implements ApplicationRunner {
    private final KafkaConsumerService service;

    public KafkaStartupRunner(KafkaConsumerService service) { this.service = service; }

    @Override
    public void run(ApplicationArguments args) { service.startConsuming(); }
}

Common Bean Lifecycle Mistakes

1. Using field-injected dependencies in the constructor. They are still null at that point. Use constructor injection or move the logic to @PostConstruct. 2. Heavy work in @PostConstruct. It blocks application startup. Keep it to validation and light setup, and push slow work to an ApplicationRunner or an async task. 3. Expecting @PreDestroy on prototype beans. Spring does not manage prototype destruction; release their resources yourself. 4. Calling @Transactional methods from @PostConstruct. The AOP proxy does not exist yet (it is created in Phase 9), so the transaction is not applied. Use ApplicationReadyEvent or a runner instead. 5. Self-invocation bypassing the proxy. Calling this.otherMethod() skips @Transactional, @Cacheable and @Async because the call never goes through the proxy. 6. Injecting a prototype into a singleton and expecting new instances. Injection happens once. Use ObjectProvider<T>, @Lookup, or ApplicationContext.getBean(). 7. Forgetting the jakarta namespace. Spring Boot 3+ uses jakarta.annotation.PostConstruct; the old javax.annotation import is silently ignored.

Frequently Asked Questions

What is the difference between @PostConstruct and a constructor? The constructor runs before field and setter injection and before any BeanPostProcessor. @PostConstruct runs after all injection is complete, so every dependency is available. What is the order of @PostConstruct, InitializingBean and init-method? @PostConstruct first, then InitializingBean.afterPropertiesSet(), then the custom init-method. Destruction order is @PreDestroy, DisposableBean.destroy(), then the custom destroy-method. Which should I use: @PostConstruct or InitializingBean? Prefer @PostConstruct. It is standard Jakarta, needs no Spring interface and keeps your class decoupled from the framework. Why is @PreDestroy not called on my bean? Common causes: the bean is prototype-scoped, the JVM was killed with SIGKILL (kill -9) instead of SIGTERM, or the method signature is invalid (it must be void with no arguments). When are AOP proxies created? In postProcessAfterInitialization (Phase 9), after @PostConstruct has run on the raw bean. Are beans created eagerly or lazily in Spring Boot? Singletons are eager by default. Enable lazy loading per bean with @Lazy or globally with spring.main.lazy-initialization=true. What is the difference between BeanPostProcessor and BeanFactoryPostProcessor? BeanFactoryPostProcessor modifies bean definitions (metadata) before any bean exists. BeanPostProcessor modifies bean instances during creation.

Key Takeaways

- Order: constructor → injection → Aware callbacks → @PostConstruct → afterPropertiesSet → init-method → in use → @PreDestroy → destroy → destroy-method. - Prefer @PostConstruct and @PreDestroy over Spring-specific interfaces. - AOP proxies appear after initialization, so @Transactional and similar annotations do not apply inside @PostConstruct. - Prototype beans are created by Spring but destroyed by you. - Use ApplicationRunner and ApplicationReadyEvent for work that must wait until the whole app is up. - Next topics: Dependency Injection, Bean Scopes, AOP Proxies, Spring Boot Auto-Configuration.