Understanding Spring Bean Lifecycle

Introduction
The Spring Framework is key for Java enterprise development. It manages “beans,” which are objects, using a smart lifecycle system. It’s important for developers to grasp this lifecycle in Spring applications. This knowledge gives them control over how resources are set up, how they behave, and how to clean them up properly.
In this article, we’ll look at the Spring bean lifecycle. We’ll discuss how beans are created and destroyed. We’ll cover both theory and practical examples. In the end, you’ll grasp how Spring handles beans. You’ll learn how to use lifecycle hooks to boost your applications.
The Spring Bean lifecycle phases
The Spring bean lifecycle can be divided into several distinct phases:
- Bean definition loading
- Bean instantiation
- Populating properties
- BeanNameAware and BeanFactoryAware callbacks
- Pre-initialization
- InitializingBean and custom init methods
- Post-initialization (BeanPostProcessor)
- Bean is ready for use
- DisposableBean and custom destroy methods
Let’s examine each of these phases in detail.
1. Bean definition loading
Spring begins by loading bean definitions. It uses XML files, Java annotations or Java configuration clasess.
@Configuration
public class AppConfig {
@Bean
public UserService userService() {
return new UserServiceImpl();
}
}
2. Bean instantiation
Spring creates an instance of the bean using one of several mechanisms:
public class UserServiceImpl implements UserService {
public UserServiceImpl() {
System.out.println("UserServiceImpl instantiated!");
}
}
3. Populating properties
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Autowired
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
System.out.println("Dependencies injected!");
}
}
4. Awareness interfaces
Spring calls specific interface methods if the bean implements awareness interfaces.
@Service
public class UserServiceImpl implements UserService, BeanNameAware, BeanFactoryAware {
private String beanName;
private BeanFactory beanFactory;
@Override
public void setBeanName(String name) {
this.beanName = name;
System.out.println("Bean name set: " + name);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
System.out.println("BeanFactory set");
}
}
5–7. Pre-initialization, Initialization and post-initilization
These phases handle bean initialization through callbacks and custom methods.
@Service
public class UserServiceImpl implements UserService, InitializingBean, DisposableBean {
// Post-construction initialization
@PostConstruct
public void init() {
System.out.println("@PostConstruct method called");
}
// InitializingBean implementation
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("afterPropertiesSet method called");
}
// Custom init method
public void customInit() {
System.out.println("Custom init method called");
}
}
8. Bean is ready for use
After initiliazation, the bean is ready for use in the application.
9. Destruction
When the application context is closed, Spring performs cleanup.
@Service
public class UserServiceImpl implements UserService, DisposableBean {
// DisposableBean implementation
@Override
public void destroy() throws Exception {
System.out.println("DisposableBean's destroy method called");
}
// Custom destroy method
@PreDestroy
public void customDestroy() {
System.out.println("@PreDestroy method called");
}
}
When to use different bean creation and lifecycle methods
Component scaning (@Component, @Service, etc)
Best for:
- Standard application components with clear responsibilities.
- When following a conventional package structure.
- General — purpose beans used across the application.
@Service
public class EmailService {
}
When to use: A tidy codebase uses standard components that clearly aligh with Spring’s stereotype annotations.
Java Configuration (@Bean methods)
Best for:
- Third — party classes you can’t modify with annotations.
- Beans that require complex initilization logic.
- Conditional bean creation.
- Beans that need different configurations in different environments.
@Configuration
public class ThirdPartyConfig {
@Bean
public ThirdPartyService thirdPartyService() {
ThirdPartyService service = new ThirdPartyService();
service.setOption("value");
return service;
}
}
When to use: When you need fine — grained control over bean creation or are integrating third — party components.
Lifecycle interfaces vs Annotations
InitializtingBean and DisposableBean interfaces
Best for:
- Framework — level components.
- When you want tight coupling withSpring
- When you need to ensure consistent initializtion/destruction behavior.
@Component
public class DatabaseConnection implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
// Initialize connection
}
@Override
public void destroy() throws Exception {
// Close connection
}
}
When to use: Use this for infrastructure parts or when developing framework — level abstractions related to Spring.
@PostConstruct and @PreDestroy
Best for:
- When working with beans defined in
@Bean
methods. - When you want to keep the class itself framework-agnostic.
- For third-party classes that already have specific initializtion/cleanup methods.
@Configuration
public class AppConfig {
@Bean(initMethod = "initialize", destroyMethod = "shutdown")
public CacheManager cacheManager() {
return new CacheManager();
}
}
When to use: Use this when adding third — party components with their own liefcycle methods. Also, use it to fully seperate your classes from Spring.
Practical implementation: Observing the bean lifecycle
Let’s create a comprehensive example that demonstrate the complete bean lifecycle. We’ll implement various lifecycle methods and log each step.
package dev.vrnsky.medium;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@Component
public class LifecycleBean implements BeanNameAware, BeanFactoryAware,
ApplicationContextAware, InitializingBean, DisposableBean {
public LifecycleBean() {
System.out.println("1. Constructor called: Bean is instantiated");
}
@Override
public void setBeanName(String name) {
System.out.println("2. BeanNameAware.setBeanName called: " + name);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("3. BeanFactoryAware.setBeanFactory called");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
System.out.println("4. ApplicationContextAware.setApplicationContext called");
}
@PostConstruct
public void postConstruct() {
System.out.println("5. @PostConstruct method called");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("6. InitializingBean.afterPropertiesSet called");
}
public void customInit() {
System.out.println("7. Custom init method called");
}
// Bean is now ready for use
public void businessMethod() {
System.out.println("Bean is in use: Business method called");
}
@PreDestroy
public void preDestroy() {
System.out.println("8. @PreDestroy method called");
}
@Override
public void destroy() throws Exception {
System.out.println("9. DisposableBean.destroy called");
}
public void customDestroy() {
System.out.println("10. Custom destroy method called");
}
}
Now, let’s setup our configuration
package dev.vrnsky;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("dev.vrnsky")
public class AppConfig {
@Bean(initMethod = "customInit", destroyMethod = "customDestroy")
public LifecycleBean lifecycleBeanWithMethods() {
return new LifecycleBean();
}
}
Let’s create a custom BeanPostProcessor to observe pre and post-initialization:
package dev.vrnsky;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof LifecycleBean) {
System.out.println("BeanPostProcessor.postProcessBeforeInitialization for " + beanName);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof LifecycleBean) {
System.out.println("BeanPostProcessor.postProcessAfterInitialization for " + beanName);
}
return bean;
}
}
Finally, let’s run our application
package dev.vrnsky;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Application {
public static void main(String[] args) {
// Create and configure the Spring container
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
// Get bean from the container
LifecycleBean bean = context.getBean(LifecycleBean.class);
// Use the bean
bean.businessMethod();
// Close the context to trigger destruction
context.close();
}
}
Custom bean lifecycle management for specific scenarios
Different types of components benefits from different lifecycle management approaches.
Resources management beans
For components managing external resources like database connections, file handles, or network sockets:
@Service
public class DatabaseConnectionService implements InitializingBean, DisposableBean {
private Connection connection;
@Value("${db.url}")
private String dbUrl;
@Value("${db.username}")
private String username;
@Value("${db.password}")
private String password;
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("Initializing database connection");
// Establish database connection
connection = DriverManager.getConnection(dbUrl, username, password);
}
public Connection getConnection() {
return connection;
}
@Override
public void destroy() throws Exception {
System.out.println("Closing database connection");
// Close the connection when the bean is destroyed
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
}
Best practice: Always use DisposableBean
or @PreDestroy
for beans that handle scarce resources. This is important in microservices, where containers often start and stop.
Caching components
For beans that perform expensive intialization to build caches:
@Component
public class ProductCache {
private Map<String, Product> cache;
@Autowired
private ProductRepository repository;
@PostConstruct
public void initializeCache() {
cache = new ConcurrentHashMap<>();
// Pre-load frequently accessed products
repository.findTopProducts().forEach(p -> cache.put(p.getId(), p));
System.out.println("Product cache initialized with " + cache.size() + " products");
}
@PreDestroy
public void persistCacheIfNeeded() {
// Persist any modified cache entries before shutdown
// This ensures no data loss when application restarts
}
}
Best practice: Use @PostConstruct
for costly cache setup, use @PreDestroy
to ensure data stays consistent before shutting down.
Lazy Initialization
For exapensive beans that aren’t always needed:
@Configuration
public class AppConfig {
@Bean
@Lazy
public ExpensiveReportGenerator reportGenerator() {
System.out.println("Creating expensive report generator - only when requested");
return new ExpensiveReportGeneratorImpl();
}
}
When to use:
- Resource — intensive to initialize.
- Not always needed during application runtime.
- Used infrequently by the application.
Conditional bean
Spring Boot provides @Conditional
annotations to create beans only when certain conditions are met:
@Configuration
public class DatabaseConfig {
@Bean
@ConditionalOnProperty(name = "db.type", havingValue = "mysql")
public DataSource mysqlDataSource() {
return new MysqlDataSource();
}
@Bean
@ConditionalOnProperty(name = "db.type", havingValue = "postgresql")
public DataSource postgresqlDataSource() {
return new PostgresqlDataSource();
}
}
When to use:
- For env — specific components.
- Feature toggles.
- Optional dependencies
- Multi — tenant applications.
Common pitfalls and best practices
Circular dependencies
Circular dependenceis occur when beans depend on each other, creating initialization problems:
@Service
public class ServiceA {
private final ServiceB serviceB;
@Autowired
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Service
public class ServiceB {
private final ServiceA serviceA;
@Autowired
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
To solve this, consider:
- Redesigning to eliminate circular dependencies.
- Using setter injection instead of constructor injection.
- Using
@Lazy
annotation.
Resource management
Always release resources in destroy methods
@Component
public class ResourceManager implements DisposableBean {
private Resource resource;
@PostConstruct
public void init() {
resource = acquireExpensiveResource();
}
@Override
public void destroy() {
if (resource != null) {
resource.release();
}
}
}
Initialization order control
When beans need to be initialized in a specific order
@Component
@DependsOn({"dataSource", "transactionManager"})
public class ReportingService {
// Implementation
}
When to use: For beans that have implicit dependencies not expressed through direct injection.
Testing consideration
For unit testing brans with lifecycle methods:
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = AppConfig.class)
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testServiceOperation() {
// Test service after lifecycle initialization has run
}
}
Best practice: Consider using @MockBean
or manual mocking for faster test that dont need to invoke lifecycle methods.
Conclusion
Understanding the Spring bean lifecycle is essential for building robust, efficient applications. Using liefcycle callbacks lets you manage how beans are set up, adjusted, and removed. This helps with resource management and keeps your application running smoothly.
Remeber these key points
- Spring manages beans through a well — defined lifecycle.
- You can hook into this lifecycle using interfaces or annotations.
- Choose the appropriate lifecycle management approach based on your component’s specific needs.
- Proper resource management is critical for application stability.
- Understanding initialization order helps avoid dependency issues
You can datek advantage of Spring’s strong dependency injection and lifecycle management for the fullest extent. This helps your create more resilient and maintanable applications.
References
1. Spring Framework Documentation
2. Spring Boot Documentation
3. Official Spring Blog