Bean Scopes & Lifecycle
A bean is not just an object: it has a lifetime. Scope decides how many instances exist and who shares them; the lifecycle decides what runs between "constructed" and "ready" and again at shutdown. This page walks every step in order, shows where proxies are born, why a prototype injected into a singleton goes stale, how to write BeanPostProcessors, how SmartLifecycle and graceful shutdown work, and proves the order with a runnable simulation.
Why Scope and Lifecycle Matter
Creating an object is the easy part. Real beans own resources: a connection pool must be opened after its properties are set and closed on shutdown; a cache must be warmed before traffic arrives; per-request data must never leak between users. Two questions decide whether that works: how many instances exist and who shares them (scope), and what runs, in what order, around creation and destruction (lifecycle).
- The default scope is singleton. Treat every singleton as a shared, concurrently used object: no per-user mutable state in fields.
- Initialisation logic belongs in a lifecycle callback or a constructor parameter, not in a constructor reading injected fields.
- Anything that holds an OS resource (thread, socket, file, pool) needs a matching shutdown callback.
One Assembly Line for Every Bean
Every bean passes through the same assembly line, driven by AbstractAutowireCapableBeanFactory.doCreateBean(). Memorise the shape: create → fill → introduce → process → initialise → process → ready → (later) destroy.
@Autowired / @Value fields and setters are filled by AutowiredAnnotationBeanPostProcessor, not by Spring core directlyBeanNameAware, BeanFactoryAware, then ApplicationContextAware and friends. Few beans need them; infrastructure does@PostConstruct. Then init methods run. "After": the last chance to replace the object, which is where proxies appearpostProcessAfterInitialization returned is what everybody receives, possibly not the object you constructed@PreDestroy, DisposableBean.destroy(), destroy-method- Reading injected fields in the constructor (step 1 vs step 2). Use constructor parameters, or move the code to
@PostConstruct. - Assuming the bean you created is the bean others hold. After step 8 other beans may hold a CGLIB subclass or JDK proxy;
thisinside the bean is still the raw object (the self-invocation trap, see AOP). - Assuming a prototype is destroyed. Steps 9 to 11 never run for prototypes; the container does not track them.
Singleton, Prototype, Request, Session
A scope is a policy implemented by org.springframework.beans.factory.config.Scope: given a name, either return the existing instance or create one through an ObjectFactory. The container asks the scope; the scope owns the cache.
| Scope | Instances | Created | Destroyed by container? | Available in |
|---|---|---|---|---|
singleton (default) | one per container | at startup (unless @Lazy) | yes, at close() | any context |
prototype | new on every request for the bean | each getBean / injection resolution | no | any context |
request | one per HTTP request | first use in the request | yes, at request end | web-aware context |
session | one per HTTP session | first use in the session | yes, at session end | web-aware context |
application | one per ServletContext | first use | yes | web-aware context |
websocket | one per WebSocket session | first use | yes | WebSocket support |
A singleton is one instance per bean definition per container. Two contexts (two tests with different configuration, a parent and child context) hold separate instances. All request threads call the same object concurrently. Safe singletons are stateless, immutable, or guard their state.
- Mutable singleton fields. Counters, "current user", reusable
SimpleDateFormat, aStringBuilder. Symptoms: intermittent wrong data under load, never reproducible in single-threaded tests. Fix: local variables, immutable objects,AtomicLong/ConcurrentHashMap, or a narrower scope. - Request/session scope outside a web request. From
@Async,@Scheduled, a message listener or@PostConstruct:IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread?. Pass the data as arguments instead. - Session beans in a stateless API. A session-scoped bean forces sticky sessions or session replication. REST services normally keep no server-side session.
- Choosing prototype for convenience. A prototype used as a factory (
newper use) is fine; a prototype expected to be cleaned up by Spring is a leak.
The Prototype-in-Singleton Trap and Scoped Proxies
Injection happens once, when the singleton is created. If a singleton receives a shorter-lived bean directly, it keeps that one instance forever, and the scope you declared is quietly ignored. This is the single most common scope bug.
For request and session scope the proxy is the idiomatic answer: a long-lived singleton must call into a short-lived bean. @RequestScope and @SessionScope already include proxyMode = TARGET_CLASS.
| Technique | Resolution time | Needs proxy-able class | Best for |
|---|---|---|---|
ObjectProvider<T> | explicit call | no | prototype factories, optional beans; clearest intent |
@Lookup | explicit call (method) | yes (subclassed) | legacy-style factory methods |
Scoped proxy (proxyMode) | every method call | yes (class not final, or an interface) | request/session beans used from singletons |
Inject ApplicationContext, call getBean | explicit call | no | avoid: couples you to Spring, hard to test |
- Scope not active. Without a proxy, a singleton asking for a request-scoped bean at startup fails:
BeanCreationException: Error creating bean with name 'scopedTarget.requestContext': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton. - Prototype with a proxy loses state between calls. Each call on the proxy builds a new instance, so
builder.add(x)thenbuilder.build()acts on two different objects. UseObjectProviderand hold the result in a local variable. - CGLIB cannot proxy a
finalclass or method. Symptom: failure to create the proxy or silently non-proxied methods. Make the class non-final, or useScopedProxyMode.INTERFACESwith an interface. thisis not the proxy. Inside the scoped bean,thisis the real instance; passing it elsewhere leaks the target outside its scope.- Threads you start yourself. A new thread has no request attributes, so request-scoped proxies fail inside it. Copy the data you need before handing work to the executor.
The Full Lifecycle, Step by Step
Below is the exact order for a singleton bean in a typical Spring Boot 3 application. Each row names the class that actually performs the step, which is what you need when a step misbehaves.
| # | Step | Performed by | Notes |
|---|---|---|---|
| 1 | Instantiate | createBeanInstance: constructor or @Bean method | constructor arguments resolved first; dependencies fully initialised already |
| 2 | Populate | AutowiredAnnotationBeanPostProcessor | @Autowired, @Value fields and setters |
| 3 | Aware (core) | invokeAwareMethods | BeanNameAware, BeanClassLoaderAware, BeanFactoryAware |
| 4 | Aware (context) + BPP before | ApplicationContextAwareProcessor and your own BPPs | EnvironmentAware, ApplicationEventPublisherAware, ApplicationContextAware, then postProcessBeforeInitialization |
| 5 | @PostConstruct | CommonAnnotationBeanPostProcessor | Spring re-registers its own annotation processors at the end of the BPP chain, so this runs after ordinary custom BPPs |
| 6 | afterPropertiesSet() | InitializingBean | ties your class to Spring |
| 7 | custom init method | @Bean(initMethod = "init") | for classes you do not own |
| 8 | BPP after | e.g. AnnotationAwareAspectJAutoProxyCreator | may return a proxy; @Async, @Scheduled and @Repository translation also hook here |
| 9 | @PreDestroy | CommonAnnotationBeanPostProcessor | at close(), singletons in reverse dependency order |
| 10 | DisposableBean.destroy() | DisposableBeanAdapter | |
| 11 | destroy-method | @Bean(destroyMethod = ...) | default is inferred: a public no-arg close() or shutdown() is called automatically |
| Mechanism | Defined on | Couples to Spring? | Prefer when |
|---|---|---|---|
@PostConstruct / @PreDestroy | your class | no (jakarta.annotation, a standard annotation) | default choice for your own classes |
InitializingBean / DisposableBean | your class | yes | framework code that must not depend on annotation processing |
@Bean(initMethod/destroyMethod) | the @Bean declaration | no | third-party classes you cannot edit |
- Importing the wrong annotation. Boot 3 uses
jakarta.annotation.PostConstruct;javax.annotation.PostConstructcompiles only if an old jar is present and is then silently ignored. Symptom: init code never runs. @Transactionalor@Asyncon a@PostConstructmethod does nothing. The callback runs on the raw object during step 5, before the proxy exists (step 8). Run startup work from anApplicationReadyEventlistener orApplicationRunnerinstead, where proxies are in place.- Long
@PostConstructblocks startup. The context does not finish refreshing, so readiness never turns green, and on Kubernetes the pod is killed. Move slow warm-ups behind anApplicationRunneror a background task with readiness gating. - Inferred destroy method surprise. A
@Beanreturning an object with a publicclose()gets it called at shutdown even if you did not ask. To prevent that on a shared resource:@Bean(destroyMethod = ""). - Exception in init.
BeanCreationException: Error creating bean with name 'x': Invocation of init method failed: the root cause is underCaused by. The whole context fails to start by design.
BeanFactoryPostProcessor vs BeanPostProcessor
Spring's own features are implemented with the same two extension points you can use. Understanding which phase each belongs to explains most "why was my bean not proxied / not processed" questions.
| BeanFactoryPostProcessor | BeanPostProcessor | |
|---|---|---|
| Operates on | BeanDefinition metadata | bean instances |
| Runs | once, before any ordinary bean is created | for every bean, around initialisation |
| Typical use | resolve ${placeholders}, change scope/lazy, register definitions | inject dependencies, run @PostConstruct, create proxies, register listeners |
| Spring examples | ConfigurationClassPostProcessor, PropertySourcesPlaceholderConfigurer | AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, AnnotationAwareAspectJAutoProxyCreator, ScheduledAnnotationBeanPostProcessor |
- "Bean X is not eligible for getting processed by all BeanPostProcessors". INFO log
Bean 'x' of type [...] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). The bean was created while post-processors were still being registered (usually because a BPP or a non-static@BeanBFPP depends on it), so it is not proxied: no@Transactional, no@Async. Fix: remove the dependency from the post-processor, or make the BPP/BFPP@Beanmethodstaticand keep it free of injected services. - A BPP returning a JDK proxy breaks by-class injection.
BeanNotOfRequiredTypeException: Bean named 'orderService' is expected to be of type 'com.acme.OrderService' but was actually of type 'jdk.proxy2.$Proxy45'. Inject the interface, or proxy with CGLIB. - Returning
nullfrompostProcessBeforeInitializationstops later processors and the bean is lost. Always return the bean or its replacement. - Heavy dependencies in a BPP. Every dependency of a BPP is created early and misses other BPPs. Keep post-processors self-contained or use
ObjectProviderfor lazy access. - Order matters. Multiple BPPs run in
PriorityOrdered, thenOrdered, then unordered sequence. ImplementOrderedif your wrapper must be inside or outside Spring's proxy creator.
Destruction, SmartLifecycle and Graceful Shutdown
At close(), AbstractApplicationContext.doClose() publishes ContextClosedEvent, stops Lifecycle beans, then destroys singletons so that dependents go before their dependencies. In Spring Boot the JVM shutdown hook is registered for you; in a plain AnnotationConfigApplicationContext you must call close() (or registerShutdownHook()), otherwise no callback ever runs.
@PostConstruct runs while the context is still being built. Work that must start after everything is wired, and stop before its collaborators go (message consumers, pollers, schedulers), implements SmartLifecycle. DefaultLifecycleProcessor starts beans in ascending getPhase() order and stops them in descending order, waiting for each phase.
| Hook | Runs | Use for | Not for |
|---|---|---|---|
@PostConstruct | step 5, during bean creation | cheap validation, derived fields | slow I/O, anything needing proxies |
ApplicationRunner / CommandLineRunner | after context refresh, before ApplicationReadyEvent | one-shot startup tasks | long-running loops (blocks startup) |
SmartLifecycle | after singletons, ordered by phase | consumers, pollers, servers with controlled stop | simple setup/teardown |
@PreDestroy | during destroy, per bean | closing a resource the bean owns | draining in-flight work across beans |
ContextClosedEvent listener | at the start of shutdown, before lifecycle stop | flip a "shutting down" flag | resource teardown |
- No shutdown hook, no callbacks. In plain Spring,
mainreturning withoutclose()skips@PreDestroy. In tests, callclose()(use try-with-resources). - Hard kills skip everything.
kill -9, OOM killer, Kubernetes afterterminationGracePeriodSeconds: set the grace period abovetimeout-per-shutdown-phaseand make work idempotent. - Non-daemon threads keep the JVM alive. A pool created in
@PostConstructand never shut down stops the process from exiting afterclose(). Pair every pool with a destroy callback. - Phase collisions. Two lifecycle beans with the default phase stop in arbitrary order. Give dependent components explicit phases.
- Blocking
stop()exceeds the phase timeout. Log:Failed to shut down 1 bean with phase value ... within timeout of 30000ms. Implementstop(Runnable)and finish within budget. - Destroying a bean others still use. A
@Beanwith an inferredclose()that wraps a shared client will close it for everyone; declaredestroyMethod = ""if the owner is elsewhere.
Application Events and Startup Sequence
The context doubles as an event bus. ApplicationEventPublisher.publishEvent(Object) delivers to every matching listener synchronously, on the publishing thread, unless you opt in to async. Events decouple "something happened" from "who cares", and are the cleanest way to break dependency cycles (see IoC & DI).
| Listener | Fires | Thread / transaction | Failure effect |
|---|---|---|---|
@EventListener | at publishEvent | publisher thread, publisher transaction | exception propagates to the publisher and can roll back its transaction |
@EventListener + @Async | at publishEvent | task executor thread, no transaction | lost unless an AsyncUncaughtExceptionHandler is set |
@TransactionalEventListener | on transaction phase (default AFTER_COMMIT) | publisher thread, transaction already completed | cannot roll back the original transaction |
ApplicationReadyEvent listener | after runners, app accepts traffic | main thread | logged; app already started |
- Synchronous means coupled. A slow or failing listener slows or fails the publisher, and runs inside its transaction. If the listener must not affect the caller, make it async or transactional-after-commit.
@TransactionalEventListenerwithout a transaction. If nothing is running a transaction when the event is published, the listener is silently not invoked unlessfallbackExecution = true.- Database writes inside AFTER_COMMIT. The old transaction resources are still bound, so a plain
@Transactionalmethod there joins an already-committed transaction and its writes are never committed. Use@Transactional(propagation = Propagation.REQUIRES_NEW)on the listener's service call. - Listening to
ContextRefreshedEventfor startup work. It fires for every refreshed context, including child contexts (and again on manualrefresh()), so the code can run twice. PreferApplicationReadyEvent. - Events published before listeners exist. A bean publishing in its constructor or
@PostConstructcan fire before@EventListenermethods are registered; listeners registered later miss it. Early events are only multicast to listeners registered at that time.
The Lifecycle in Plain Java
This simulation re-implements the assembly line in plain Java so you can see the order. It has a BeanFactoryPostProcessor that runs before any bean exists, a BeanPostProcessor whose "after" hook replaces OrderService with a proxy subclass, reflection-driven @PostConstruct/@PreDestroy, BeanNameAware, InitializingBean/DisposableBean, custom init/destroy methods, destruction in reverse creation order, and a prototype scope demo. It uses no Spring classes; names are borrowed so the mapping is obvious.
- Line 1: the factory post-processor ran before any constructor: definitions are edited before objects exist.
- Lines 2 to 9: the exact order from chapter 5: constructor, populate, aware, BPP before,
@PostConstruct,afterPropertiesSet, init-method, BPP after. - Lines 10 to 14:
orderServiceis created afterpoolbecause its factory needs it: a dependency is fully initialised before its dependent. injected bean class: AuditProxy: BPP-after returned a replacement, and that is what the container stored. The callbacks on lines 16 to 19 still ran on the raw instances, and in reverse order:orderServicefirst, thenpool.stale: 1, 1, 1vsfresh: 2, 3, 4: the prototype resolved once at injection time versus once per call through a provider.
- Real Spring runs
@Autowired/@PostConstructthrough BPPs too; here they are hard-coded steps. The visible order is the same. - Real Spring tracks disposable beans in
DisposableBeanAdapters and handles dependent-bean ordering explicitly (registerDependentBean); the simulation reuses creation order. - Proxies in real Spring are CGLIB or JDK proxies created by
AbstractAutoProxyCreator; see AOP & Proxies for a hand-written one.
Ten Questions on Scopes and Lifecycle
What are the bean scopes, and what is the default?
singleton (default), prototype, and in web contexts request, session, application, websocket. You can register custom scopes (CustomScopeConfigurer).
Is a singleton bean thread-safe?
No. Spring guarantees one instance per container, nothing more. Keep it stateless/immutable or synchronise state. This is why per-request data lives in locals or request-scoped beans.
What happens when a prototype is injected into a singleton?
It is injected once at singleton creation, so the singleton keeps that one instance. Use ObjectProvider, @Lookup or a scoped proxy to get a fresh one per use.
How does a request-scoped bean work inside a singleton?
The singleton holds a CGLIB scoped proxy (@RequestScope sets proxyMode=TARGET_CLASS). Every method call resolves the target from the current thread's request attributes.
Give the lifecycle order of a bean.
Instantiate, populate, Aware callbacks, BPP before, @PostConstruct, afterPropertiesSet, init-method, BPP after (proxy), ready; on close @PreDestroy, destroy(), destroy-method.
BeanFactoryPostProcessor vs BeanPostProcessor?
BFPP edits definitions before any bean exists (placeholders, @Configuration parsing). BPP wraps every bean instance around initialisation (injection, @PostConstruct, proxies).
Why does @Transactional on a @PostConstruct method not work?
The callback runs on the raw object before postProcessAfterInitialization creates the proxy, so no advice applies. Run it from a listener on ApplicationReadyEvent or a runner.
Are destroy callbacks called for prototype beans?
No. The container creates and initialises them, then forgets them. You must release their resources yourself.
How do you start a consumer after everything is wired and stop it first on shutdown?
Implement SmartLifecycle with a phase; start is ascending phase, stop is descending, and stop(Runnable) lets you signal asynchronous completion. Combine with server.shutdown=graceful.
What is the difference between sync @EventListener and @TransactionalEventListener?
The first runs inside publishEvent in the publisher's transaction. The second is bound to a transaction phase (default AFTER_COMMIT) and is skipped if there is no transaction, unless fallbackExecution=true.
- Trap: "@PostConstruct runs after the proxy is created." No, before; that is why proxy-based features do not apply there.
- Trap: "singleton scope = GoF singleton." Per container and per definition, not per JVM or classloader.
- Trap: "prototype beans get @PreDestroy." Only init callbacks run.
Summary & Cheat Sheet
| Problem | Reach for | Why |
|---|---|---|
| Shared mutable state in a service | locals, immutable objects, or request scope | singleton = concurrent use |
| Fresh object per call from a singleton | ObjectProvider<T> | lookup at call time |
| Per-request data used deep in the call chain | @RequestScope bean (proxied) | avoids threading context through every signature |
| Init that needs injected fields | @PostConstruct | runs after populate |
| Init of a third-party class | @Bean(initMethod = ...) | no source access |
| Wrap or replace beans generically | BeanPostProcessor | after-init hook returns the proxy |
| Change definitions before creation | BeanFactoryPostProcessor (static @Bean) | definition phase |
| Ordered start/stop of workers | SmartLifecycle + phase | defined start and graceful stop |
| Decouple a side effect from a transaction | @TransactionalEventListener | runs only after commit |
- Default scope is singleton: shared and concurrent. Never keep per-request state in fields.
- Injection happens once. Shorter-lived beans in a singleton need
ObjectProvider,@Lookupor a scoped proxy. - Lifecycle order: constructor, populate, Aware, BPP before,
@PostConstruct,afterPropertiesSet, init-method, BPP after (proxy), ready;@PreDestroy,destroy, destroy-method. - Proxies are made in BPP-after, so init callbacks see the raw object.
- BFPP edits recipes; BPP wraps dishes. Keep both free of heavy dependencies, and make BFPP
@Beans static. - Prototypes are never destroyed by Spring. Singletons are destroyed in reverse dependency order, only if the context is closed gracefully.
- Use SmartLifecycle and graceful shutdown for anything that must drain work before the JVM exits.
- Events are synchronous by default. Use after-commit listeners for side effects that must follow a successful transaction.
- 01 IoC & Dependency Injection: how beans get resolved and wired before this assembly line starts.
- 03 AOP & Proxies: what the BPP-after proxy actually does on each call.
- 08 Transactions: transaction-bound events and the AFTER_COMMIT rules in depth.
- 12 Actuator & Observability: liveness/readiness state changes published as events.