Skip to content
Spring Boot sb core 4 min read

Beans & Lifecycle

A bean is any object the Spring IoC container creates and manages. The container does far more than call a constructor: it walks each bean through a well-defined lifecycle, giving you hooks to run setup logic after dependencies are injected and cleanup logic before shutdown. Understanding this sequence lets you initialize caches, open connections, and release resources at exactly the right moment.

The lifecycle phases

When the container starts, every singleton bean passes through these phases in order:

  1. Bean definition — the container reads metadata from stereotype annotations, @Bean methods, and auto-configuration.
  2. Instantiation — the container calls the constructor (or factory method) to create the raw object.
  3. Dependency injection — constructor arguments, setters, and @Autowired fields are populated.
  4. Aware callbacks — interfaces such as BeanNameAware and ApplicationContextAware receive infrastructure references.
  5. BeanPostProcessor.postProcessBeforeInitialization — runs against the fully-injected bean.
  6. Initialization@PostConstruct, then InitializingBean.afterPropertiesSet(), then any custom initMethod.
  7. BeanPostProcessor.postProcessAfterInitialization — last chance to wrap the bean (this is where AOP proxies are created).
  8. Bean is ready — the container hands it out for injection and use.
  9. Destruction — on context shutdown: @PreDestroy, then DisposableBean.destroy(), then any custom destroyMethod.

Note: Destruction callbacks only fire for beans the container owns and only for singleton-scoped beans. The container does not manage the full lifecycle of prototype beans — see Bean Scopes.

Initialization and destruction callbacks

The idiomatic, framework-agnostic approach uses the JSR-250 annotations @PostConstruct and @PreDestroy from the jakarta.annotation package.

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;

@Component
public class ConnectionPool {

    @PostConstruct
    public void init() {
        System.out.println("ConnectionPool: opening connections");
    }

    @PreDestroy
    public void shutdown() {
        System.out.println("ConnectionPool: closing connections");
    }
}

@PostConstruct runs after all dependencies are injected, so it is safe to use injected collaborators there — something a constructor cannot always guarantee for proxied beans.

InitializingBean and DisposableBean

Spring also exposes two callback interfaces. They couple your class to the framework, so the annotations above are usually preferred, but you will encounter these in framework code.

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

@Component
public class CacheWarmer implements InitializingBean, DisposableBean {

    @Override
    public void afterPropertiesSet() {
        System.out.println("CacheWarmer: warming cache");
    }

    @Override
    public void destroy() {
        System.out.println("CacheWarmer: evicting cache");
    }
}

A third option is the @Bean attributes initMethod and destroyMethod, ideal for third-party classes you cannot annotate:

@Configuration
public class AppConfig {

    @Bean(initMethod = "start", destroyMethod = "stop")
    public MessageBroker broker() {
        return new MessageBroker();
    }
}

Tip: Within a single bean the order is fixed: @PostConstructafterPropertiesSet()initMethod, and on shutdown @PreDestroydestroy()destroyMethod. Pick one mechanism per concern to keep behavior obvious.

BeanPostProcessor

A BeanPostProcessor lets you inspect or modify every bean as it is initialized. Spring itself uses these to process annotations and build AOP proxies. Your own implementation runs around each bean’s initialization phase.

import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class TimingPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String name) {
        System.out.println("Before init: " + name);
        return bean; // returning bean leaves it unchanged
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String name) {
        System.out.println("After init: " + name);
        return bean; // could return a proxy wrapping the bean
    }
}

Warning: A BeanPostProcessor runs against virtually every bean in the context, so it must be cheap and must never throw for beans it does not care about. Always return the incoming bean if you are not transforming it.

Putting it together

Given the ConnectionPool, CacheWarmer, and TimingPostProcessor above, starting and then stopping the application produces a clear ordering. Initialization callbacks fire as each bean is created; destruction callbacks fire in reverse on shutdown.

@SpringBootApplication
public class LifecycleApplication {
    public static void main(String[] args) {
        // try-with-resources closes the context, triggering destruction
        try (var ctx = SpringApplication.run(LifecycleApplication.class, args)) {
            System.out.println("Application running...");
        }
    }
}

Output:

Before init: connectionPool
ConnectionPool: opening connections
After init: connectionPool
Before init: cacheWarmer
CacheWarmer: warming cache
After init: cacheWarmer
Application running...
CacheWarmer: evicting cache
ConnectionPool: closing connections

Note how the post-processor brackets each bean’s @PostConstruct, and how destruction happens in reverse creation order so dependents shut down before their dependencies.

Best practices

  • Use @PostConstruct / @PreDestroy for most setup and teardown — they are clean and framework-neutral.
  • Keep initialization fast; long-running startup work should be moved to an ApplicationRunner or done asynchronously.
  • Reserve BeanPostProcessor for cross-cutting infrastructure, not business logic.
  • Remember that prototype beans receive initialization callbacks but no destruction callbacks.
Last updated June 13, 2026
Was this helpful?