Bean Lifecycle and Scopes
Every bean goes through a fixed life: it is created, filled with dependencies, initialized, used and finally destroyed. You can run your own code at these points. Scope decides how many instances exist.
Bean lifecycle
1. Instantiate the bean (constructor)
2. Inject dependencies
3. Aware callbacks (BeanNameAware, ApplicationContextAware ...)
4. BeanPostProcessor.postProcessBeforeInitialization
5. @PostConstruct → InitializingBean.afterPropertiesSet() → custom init-method
6. BeanPostProcessor.postProcessAfterInitialization (proxies for @Transactional, @Async are created here)
7. ───── bean is ready and used ─────
8. @PreDestroy → DisposableBean.destroy() → custom destroy-method (on shutdown)import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component
public class CacheWarmer {
public CacheWarmer() { System.out.println("1. constructor"); }
@PostConstruct
public void init() { System.out.println("2. @PostConstruct: load data"); }
@PreDestroy
public void cleanup() { System.out.println("3. @PreDestroy: release resources"); }
}Scopes
| Scope | Instances | Notes |
|---|---|---|
| singleton (default) | One per container | Shared by all. Must be thread-safe (no changing fields) |
| prototype | New one for each injection or getBean call | Spring does not call @PreDestroy for it |
| request | One per HTTP request | Web apps only |
| session | One per HTTP session | Web apps only |
| application | One per ServletContext | Web apps only |
@Component
@Scope("prototype")
public class ShoppingCart { }
@Service
public class CartFactory {
private final ObjectProvider<ShoppingCart> cartProvider;
public CartFactory(ObjectProvider<ShoppingCart> cartProvider) {
this.cartProvider = cartProvider;
}
public ShoppingCart newCart() {
return cartProvider.getObject(); // a NEW cart each time
}
}Tip: If you inject a prototype bean directly into a singleton, the singleton gets only ONE instance when it is created. Use ObjectProvider (or a scoped proxy) to get a new instance each time.
@Bean methods and @Configuration
@Configuration(proxyBeanMethods = false) // faster startup when @Bean methods do not call each other
public class AppConfig {
@Bean
public Clock clock() {
return Clock.systemUTC();
}
@Bean(initMethod = "start", destroyMethod = "stop")
public Scheduler scheduler(Clock clock) {
return new Scheduler(clock);
}
}Run code when the application starts
@Component
public class StartupRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
System.out.println("Application started with args: " + args.getOptionNames());
}
}
@Component
public class ReadyListener {
@EventListener(ApplicationReadyEvent.class)
public void onReady() { System.out.println("Ready to accept requests"); }
}See the lifecycle order in a log
@Component
public class LifecycleDemo implements BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean, SmartInitializingSingleton {
public LifecycleDemo() { System.out.println("1 constructor"); }
@Autowired public void setEnvironment(Environment env) { System.out.println("2 dependency injected"); }
@Override public void setBeanName(String name) { System.out.println("3 BeanNameAware: " + name); }
@Override public void setApplicationContext(ApplicationContext ctx) { System.out.println("4 ApplicationContextAware"); }
@PostConstruct public void postConstruct() { System.out.println("5 @PostConstruct"); }
@Override public void afterPropertiesSet() { System.out.println("6 afterPropertiesSet"); }
@Override public void afterSingletonsInstantiated() { System.out.println("7 all singletons ready"); }
@PreDestroy public void preDestroy() { System.out.println("8 @PreDestroy"); }
@Override public void destroy() { System.out.println("9 DisposableBean.destroy"); }
}1 constructor
2 dependency injected
3 BeanNameAware: lifecycleDemo
4 ApplicationContextAware
5 @PostConstruct
6 afterPropertiesSet
7 all singletons ready (after ALL singleton beans are created)
... application runs ...
8 @PreDestroy (on shutdown: destruction happens in the reverse order of creation)
9 DisposableBean.destroy| Aware interface | Gives your bean |
|---|---|
| BeanNameAware | Its own bean name |
| BeanFactoryAware / ApplicationContextAware | The container (use rarely: it couples you to Spring) |
| EnvironmentAware | Properties and profiles |
| ResourceLoaderAware | Load classpath: and file: resources |
| ApplicationEventPublisherAware | Publish events |
Request scope inside a singleton: scoped proxies
@Component
@RequestScope // = @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String correlationId = UUID.randomUUID().toString();
public String getCorrelationId() { return correlationId; }
}
@Service // singleton
public class AuditService {
private final RequestContext context; // a PROXY is injected once. Each call is routed to the bean of the CURRENT request
public AuditService(RequestContext context) { this.context = context; }
public void log(String msg) { System.out.println(context.getCorrelationId() + " " + msg); }
}Ordered start and stop: SmartLifecycle
@Component
public class QueueConsumerLifecycle implements SmartLifecycle {
private volatile boolean running;
@Override public void start() { running = true; /* start consuming */ }
@Override public void stop() { running = false; /* finish current work, close resources */ }
@Override public boolean isRunning() { return running; }
@Override public int getPhase() { return Integer.MAX_VALUE - 100; } // start late, stop EARLY (reverse order on shutdown)
}On shutdown the order is: web server stops accepting requests (graceful shutdown), lifecycle beans stop from the highest phase to the lowest, then singleton beans are destroyed in reverse creation order. This is why a consumer with a high phase stops before the database pool closes.
Choosing the right scope
| Need | Use |
|---|---|
| Stateless service, repository, controller | singleton (default). No mutable instance state |
| Object with per-use state that Spring should create | prototype via ObjectProvider or @Lookup |
| Data of one HTTP request | @RequestScope |
| User data across requests | @SessionScope (or, better, keep it in a database or cache) |
| Thread-confined data | Avoid ThreadLocal in singletons with thread pools, or clear it in a finally block |
Common mistake: Keeping user-specific state in fields of a singleton bean (for example the current user name). All requests share the same instance, so data mixes between users. Keep singletons stateless.
Interview questions
Is a Spring singleton the same as the singleton design pattern?
No. A Spring singleton is one instance per container. The design pattern is one instance per JVM/class loader.
When should I use @PostConstruct?
For initialization that needs injected dependencies, for example loading data into a cache. The constructor runs before injection is finished.
What is the difference between @Bean and @Component?
@Component is put on your own class and found by scanning. @Bean is a method in a configuration class, useful for third-party classes you cannot annotate.
Does Spring call @PreDestroy for prototype beans?
No. Spring creates the prototype and hands it over. The caller owns it and must release its resources.
In what order are beans destroyed?
In the reverse order of their creation, so a bean is destroyed before the beans it depends on.