Spring Boot
Bean Lifecycle
Every Spring bean goes through a precise lifecycle — from BeanDefinition registration through instantiation, dependency injection, initialization callbacks, active use, and finally destruction. Understanding this lifecycle lets you write correct initialization and cleanup code, and diagnose subtle bugs around bean state and ordering.
The Complete Bean Lifecycle
A Spring bean's lifecycle has twelve distinct phases from configuration reading to destruction. Each phase has hooks that let you plug in custom behavior at precisely the right moment.
Phase 1 — BeanDefinition creation: Spring reads your annotations and configuration, creating a BeanDefinition for each bean — metadata about how to create it, what scope it has, what its dependencies are.
Phase 2 — BeanFactoryPostProcessor: Processors run against BeanDefinitions before any beans are instantiated. PropertySourcesPlaceholderConfigurer (which resolves ${...} placeholders) runs here.
Phase 3 — Instantiation: Spring calls the constructor, creating the object. Dependencies are not injected yet.
Phase 4 — Dependency injection: Spring injects all @Autowired fields, calls @Autowired setters, and performs @Value injection.
Phase 5 — BeanPostProcessor (before init): All BeanPostProcessors run their postProcessBeforeInitialization method. AOP proxies, @Autowired processing, and validation happen here.
Phase 6 — @PostConstruct: Your initialization method runs. All dependencies are injected and available.
Phase 7 — InitializingBean.afterPropertiesSet(): If your bean implements InitializingBean, this method is called.
Phase 8 — Custom init-method: If you declared an initMethod in @Bean(initMethod="..."), it's called here.
Phase 9 — BeanPostProcessor (after init): postProcessAfterInitialization runs. This is where AOP proxies are actually created — the proxy wraps your bean.
Phase 10 — Bean in use: The bean is ready, registered in the context, and injectable everywhere.
Phase 11 — @PreDestroy: On shutdown, your cleanup method runs first.
Phase 12 — DisposableBean.destroy(): If implemented, 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-methodDestruction 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. You can listen to these events to run code at precise moments — after context refresh, before shutdown, or when specific beans become available.
Java
// Listen to lifecycle events with @EventListener:
@Component
@Slf4j
public class ApplicationLifecycleListener {
// Called after the context is fully refreshed — all beans created and ready:
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady(ApplicationReadyEvent event) {
log.info("Application fully started — all beans initialized");
// Safe to make external connections, start schedulers, warm caches
}
// Called when context starts closing (before @PreDestroy methods):
@EventListener(ContextClosedEvent.class)
public void onContextClosing(ContextClosedEvent event) {
log.info("Context closing — flushing pending operations");
}
// Called when context is refreshed (earlier than ApplicationReadyEvent):
@EventListener(ContextRefreshedEvent.class)
public void onContextRefreshed(ContextRefreshedEvent event) {
log.info("Context refreshed");
}
}
// ApplicationRunner — runs code after 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) {
log.info("Running data migration...");
migrationService.migrate();
log.info("Data migration complete");
}
}
// CommandLineRunner — simpler version (receives 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) throws Exception {
log.info("Warming caches...");
cacheService.warmAll();
log.info("Cache warmup complete");
}
}
// SmartLifecycle — fine-grained control over start/stop:
@Component
public class BackgroundJobScheduler implements SmartLifecycle {
private volatile boolean running = false;
@Override
public void start() {
running = true;
// Start background threads, schedulers, consumers
}
@Override
public void stop() {
running = false;
// Stop gracefully
}
@Override
public boolean isRunning() { return running; }
@Override
public int getPhase() { return 0; } // lower phases start first, stop 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
Here's a realistic bean that uses multiple lifecycle hooks together — showing the order they execute and the right things to do at each phase.
Java
@Service
@Slf4j
public class KafkaConsumerService implements InitializingBean, DisposableBean {
private final KafkaProperties kafkaProperties;
private final OrderService orderService;
private KafkaConsumer<String, String> consumer;
private ExecutorService executorService;
private volatile boolean consuming = false;
// Phase 3 — INSTANTIATION: constructor called, no injection yet
public KafkaConsumerService(KafkaProperties kafkaProperties,
OrderService orderService) {
this.kafkaProperties = kafkaProperties;
this.orderService = orderService;
log.info("KafkaConsumerService constructed");
// DON'T use kafkaProperties here if it came from @Value
// @Value injection hasn't happened yet for field-injected values
// Constructor-injected values ARE available here
}
// Phase 6 — @PostConstruct: all injection complete
@PostConstruct
public void postConstruct() {
log.info("@PostConstruct: validating configuration");
// Safe to use all injected dependencies here
if (kafkaProperties.getBootstrapServers() == null) {
throw new IllegalStateException("Kafka bootstrap servers not configured");
}
// Do lightweight validation — NOT heavy initialization
}
// Phase 7 — InitializingBean: same timing, called after @PostConstruct
@Override
public void afterPropertiesSet() {
log.info("afterPropertiesSet: creating Kafka consumer");
Properties props = new Properties();
props.put("bootstrap.servers", kafkaProperties.getBootstrapServers());
props.put("group.id", kafkaProperties.getGroupId());
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
this.consumer = new KafkaConsumer<>(props);
this.consumer.subscribe(List.of("orders"));
}
// Called by ApplicationRunner after ALL beans are ready:
public void startConsuming() {
consuming = true;
executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
log.info("Kafka consumer loop started");
while (consuming) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(100));
records.forEach(record -> {
orderService.processOrderEvent(record.value());
consumer.commitSync();
});
}
});
log.info("Kafka consuming started on topic: orders");
}
// Phase 11 — @PreDestroy: first destruction hook
@PreDestroy
public void preDestroy() {
log.info("@PreDestroy: stopping consumer loop");
consuming = false;
}
// Phase 12 — DisposableBean: called after @PreDestroy
@Override
public void destroy() throws Exception {
log.info("destroy: shutting down executor and closing consumer");
if (executorService != null) {
executorService.shutdown();
executorService.awaitTermination(10, TimeUnit.SECONDS);
}
if (consumer != null) {
consumer.close(Duration.ofSeconds(5));
}
log.info("Kafka consumer cleanly shut down");
}
}
// ApplicationRunner that starts consuming after all beans are ready:
@Component
@Order(Ordered.LOWEST_PRECEDENCE) // run last — after all other runners
public class KafkaStartupRunner implements ApplicationRunner {
private final KafkaConsumerService kafkaConsumerService;
public KafkaStartupRunner(KafkaConsumerService kafkaConsumerService) {
this.kafkaConsumerService = kafkaConsumerService;
}
@Override
public void run(ApplicationArguments args) {
kafkaConsumerService.startConsuming();
}
}