Understanding Spring Bean Lifecycle

Coffee beans, but today about different beans

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:

  1. Bean definition loading
  2. Bean instantiation
  3. Populating properties
  4. BeanNameAware and BeanFactoryAware callbacks
  5. Pre-initialization
  6. InitializingBean and custom init methods
  7. Post-initialization (BeanPostProcessor)
  8. Bean is ready for use
  9. 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:

  1. Redesigning to eliminate circular dependencies.
  2. Using setter injection instead of constructor injection.
  3. 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

Subscribe to Egor Voronianskii | Java Development and whatsoever

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe