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:
- Bean definition — the container reads metadata from stereotype annotations,
@Beanmethods, and auto-configuration. - Instantiation — the container calls the constructor (or factory method) to create the raw object.
- Dependency injection — constructor arguments, setters, and
@Autowiredfields are populated. - Aware callbacks — interfaces such as
BeanNameAwareandApplicationContextAwarereceive infrastructure references. BeanPostProcessor.postProcessBeforeInitialization— runs against the fully-injected bean.- Initialization —
@PostConstruct, thenInitializingBean.afterPropertiesSet(), then any custominitMethod. BeanPostProcessor.postProcessAfterInitialization— last chance to wrap the bean (this is where AOP proxies are created).- Bean is ready — the container hands it out for injection and use.
- Destruction — on context shutdown:
@PreDestroy, thenDisposableBean.destroy(), then any customdestroyMethod.
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
prototypebeans — 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:
@PostConstruct→afterPropertiesSet()→initMethod, and on shutdown@PreDestroy→destroy()→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
BeanPostProcessorruns 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 incomingbeanif 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/@PreDestroyfor 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
BeanPostProcessorfor cross-cutting infrastructure, not business logic. - Remember that
prototypebeans receive initialization callbacks but no destruction callbacks.