The Complete Bean Lifecycle: From Instantiation to Destruction
A Java object exists the moment you new it, but "exists" and "is safe to use" are two different things. When the constructor returns, your @Autowired fields may still be null; once they are filled you may still need to validate config, warm caches or open connections; and before the process exits those resources must be released. Spring standardises the whole road from "born" to "usable" to "retired" into eight phases and leaves a socket at each one for your own code — that is the bean lifecycle.
Five words, one line each (used throughout):
- Bean: not an object Spring invented, but your plain Java object that you handed to the container to create and manage
- Container: the "housekeeper" object (
ApplicationContext) that news your objects, wires their dependencies and manages birth to death; you ask it for things withgetBean - Reflection: the technique of "calling a constructor or method by name at runtime" — the container never hard-codes your class, it builds your object through reflection
- Hook: a slot the framework reserves inside its own flow; put a method in it and the container calls you when it reaches that step
@PostConstruct: an annotation meaning "all dependencies are filled — run this method before I am used from outside"
think of a bean as a human life. Birth (instantiation, the constructor runs) → growing up (population, absorbing the nutrition your parents hand you) → getting an ID card (Aware callbacks: "what am I called, which container do I live in") → the medical check before school starts (initialization: is my config right, am I fit for duty) → being issued a résumé and a staff badge at graduation (the AOP proxy — what other people actually meet is the badged version of you) → decades of work (ready & pooled, called over and over) → retirement and settling the estate (destroy callbacks, giving back every resource you held).

Read that timeline left to right and you have the real growth path of a singleton: the further left, the fewer injected fields you can safely read. Section 3 turns it into runnable log output.
After this article you should be able to answer three questions:
- Which runs first,
@PostConstruct,afterPropertiesSetorinitMethod— and why that order? - Why does a prototype bean's
@PreDestroynever print? Didn't the container promise to manage it? - At which step is the AOP proxy created, and how does that decide whether
@Transactionalworks inside a constructor?
Start with a "why": why do lifecycle callbacks exist at all? Because an object being "created" and an object being "safe to use" are two different things. When the constructor returns, the object only has memory — dependencies are not injected yet. After injection you may still need to validate config, warm caches or open connections; and when done, you must release resources. Spring standardizes these "insertion points" into eight stages of life.

The eight stages in order: instantiation → population → Aware callbacks → before-init processing → initialization → after-init processing → ready & pooled → destroy callbacks. Note it is a "ring", not a straight line — a singleton goes round once and then lives in the container until it shuts down and takes the final destroy stage.
this article focuses on the singleton lifecycle. A prototype bean walks only up to "ready" and is then handed to the caller; the container does not manage its destruction — Section 5 covers this.

Here are "who triggers it, where, and what for" in one table:
| Stage | Interface / annotation | Source location | Typical use |
|---|---|---|---|
| Instantiation | Constructor / factory method | AbstractAutowireCapableBeanFactory#createBeanInstance | Pick a constructor, reflect with newInstance |
| Population | @Autowired / @Value / XML property | #populateBean → AutowiredAnnotationBeanPostProcessor | Push dependencies into fields or setters |
| Aware callbacks | BeanNameAware / BeanFactoryAware / ApplicationContextAware | #invokeAwareMethods + ApplicationContextAwareProcessor | Let the bean learn "who am I, which container" |
| Before-init | BeanPostProcessor#postProcessBeforeInitialization | First loop inside #initializeBean | @PostConstruct fires here via CommonAnnotationBeanPostProcessor |
| Initialization | InitializingBean#afterPropertiesSet / @Bean(initMethod) | #invokeInitMethods | Validate config, warm caches, open resources |
| After-init | BeanPostProcessor#postProcessAfterInitialization | Second loop inside #initializeBean | AOP proxy creation (AbstractAutoProxyCreator) |
| Ready & pooled | — | DefaultSingletonBeanRegistry#addSingleton | Stored in singletonObjects for global reuse |
| Destroy | @PreDestroy / DisposableBean#destroy / @Bean(destroyMethod) | DisposableBeanAdapter#destroy | Close connections, flush, release pools |
@PostConstruct is not a separate stage — it is essentially a BeanPostProcessor callback during the "before-init" stage. Only by grasping this can you explain the classic trap in Section 7.
The table says who triggers each stage; what actually stalls beginners is what is readable at that moment. Can I use an injected field in the constructor? Is the proxy there yet inside @PostConstruct? Click the eight stations one by one — each gives one honest answer:
The ring answers which stations exist; the debugger answers who pushes you along them. On the left is the real skeleton of getBean → doGetBean → doCreateBean; the right panel tracks variables and the call stack as you step. Two things to watch: the cache hit at step ②, and where the pool insert happens at step ⑦:
Object getBean("userService") // all you wroteObject cached = getSingleton(beanName); // 1. ask the primary cache: already there?if (cached != null) return getObjectForBeanInstance(cached); // 2. hit -> short circuitRootBeanDefinition mbd = getMergedBeanDefinition(beanName); // 3. miss: open the fileObject obj = new UserService(); // 4. doCreateBean: instantiatepopulateBean(obj, mbd); // 5. population: @Autowired appliesinitializeBean(obj, beanName, mbd); // 6. Aware + init + proxyingaddSingleton(beanName, obj); // 7. publish into the primary cachereturn getSingleton(beanName); // 8. hand it to the caller| beanName | userService |
| singletonObjects | {orderService=..., ...} |
| allowCircularReferences | false |
getBeandoGetBeanFirst call versus second call, drawn once more as an animation — notice how frame ④ (cache hit) and frame ⑤ (prototypes never pool) jointly produce every destroy symptom in Section 5:

Talk is cheap; run it. This bean implements three callback interfaces and layers annotation callbacks on top:
package com.example.lifecycle;import org.springframework.beans.factory.BeanNameAware;import org.springframework.beans.factory.DisposableBean;import org.springframework.beans.factory.InitializingBean;import javax.annotation.PostConstruct;import javax.annotation.PreDestroy;public class LifecycleDemoBean implements BeanNameAware, InitializingBean, DisposableBean { private String beanName; public LifecycleDemoBean() { System.out.println("[1] constructor: instantiation"); } @Override public void setBeanName(String name) { this.beanName = name; System.out.println("[2] BeanNameAware#setBeanName -> " + name); } @PostConstruct public void postConstruct() { System.out.println("[3] @PostConstruct"); } @Override public void afterPropertiesSet() { System.out.println("[4] InitializingBean#afterPropertiesSet"); } public void customInit() { System.out.println("[5] initMethod (customInit)"); } @PreDestroy public void preDestroy() { System.out.println("[6] @PreDestroy"); } @Override public void destroy() { System.out.println("[7] DisposableBean#destroy"); } public void customDestroy() { System.out.println("[8] destroyMethod (customDestroy)"); }}The config class and the bootstrap code:
@Configurationpublic class LifecycleConfig { @Bean(initMethod = "customInit", destroyMethod = "customDestroy") public LifecycleDemoBean demoBean() { return new LifecycleDemoBean(); }}public class Main { public static void main(String[] args) { // try-with-resources auto-calls close() at block end, triggering destruction try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(LifecycleConfig.class)) { System.out.println("[*] container ready, safe to fetch beans"); } System.out.println("[*] container closed"); }}The console output mapped onto the stages:
[1] constructor: instantiation <- stage 1: instantiation[2] BeanNameAware#setBeanName -> demoBean <- stage 3: Aware callbacks[3] @PostConstruct <- stage 5: init (fired by a BeanPostProcessor)[4] InitializingBean#afterPropertiesSet <- stage 5: init (after @PostConstruct)[5] initMethod (customInit) <- stage 5: init (runs last)[*] container ready, safe to fetch beans[6] @PreDestroy <- stage 8: destroy (runs first)[7] DisposableBean#destroy <- stage 8: destroy[8] destroyMethod (customDestroy) <- stage 8: destroy (runs last)[*] container closed- Init callbacks have a fixed order:
@PostConstruct→afterPropertiesSet→initMethod - Destroy callbacks have a fixed order:
@PreDestroy→destroy→destroyMethod - Note
[2]precedes[3]: Aware callbacks run before any init callback
Now replay that log against the animation — each frame maps onto one output line: [1] is frame one, [6][7][8] are the final frame. When a second read of the code stalls, use it to locate "which line am I on":

if you change the bean's scope to prototype, lines [6][7][8] never print. That is not a bug — it is the destroy symmetry we cover next.
This is a favorite interview follow-up. The answer is precise: during the "after-init" stage, by AbstractAutoProxyCreator#postProcessAfterInitialization.
// AbstractAutoProxyCreator (parent of AbstractAdvisorAutoProxyCreator)@Overridepublic Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) != bean) { return wrapIfNecessary(bean, beanName, cacheKey); // wrap into a proxy if needed } } return bean;}wrapIfNecessarychecks whether the bean matches any advice (@Transactional,@Cacheable...), and if so aProxyFactorybuilds a JDK or CGLIB proxy- The proxy is "pulled over" the original object only after its initialization completes, so the proxy's
targetpoints at that fully wired object - Consequently: whatever you inject elsewhere, whatever you fetch from the container, is the proxy — not the raw object you
new-ed
Trap: this explains a beginner's confusion — "getClass() prints UserService$$EnhancerBySpringCGLIB, but I wrote UserService". Because the proxy is the very thing that got swapped into the singleton pool.
The destroy stage is full of symmetry — and traps. Four symptoms, one underlying cause (whether the container still holds a reference). Pair them up first and the three rules below become obvious:
- Why are prototypes not destroyed? Because the container lets go immediately after creating one — it keeps no reference, so there is nothing to destroy. Release resources yourself with
try-with-resources - Why reverse order for singletons?
DefaultSingletonBeanRegistryrecords order indisposableBeans, anddestroySingletons()iterates backwards - If
@PreDestroydid not run, debug in three steps: "wasclose()called → is it a singleton → was it ever created"
Reverse ordering is hard to feel from prose alone, so the animation plays "who was built first, who is torn down first" — watch frames ③ and ④ in particular:

in Spring Boot, @PreDestroy relies on close() being triggered on a normal JVM shutdown. If the process is force-killed with kill -9, destroy callbacks will not run — critical data must rely on try/finally or an external safety net, never on @PreDestroy alone.
Isolate the single most misremembered fork — singleton versus prototype — into one side-by-side picture: on the left the singleton that walks the pipeline once and then sits in the pool; on the right the prototype that re-runs everything per lookup while the container lets go immediately:

a singleton is the office shared printer — configured once at installation (steps 1–7), used by everyone afterwards, and the company (the container) maintains it and eventually scraps it. A prototype is a disposable takeaway box — you get a fresh one per order, and no courier comes back to collect it. Whether you rinse the box before binning it (releasing a prototype's resources) is therefore always your own business.
Want to find the number-one suspect for slow startup? One custom BeanPostProcessor suffices — it straddles both ends of the lifecycle:
package com.example.lifecycle;import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;import org.springframework.stereotype.Component;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;@Componentpublic class TimingBeanPostProcessor implements InstantiationAwareBeanPostProcessor { private final Map<String, Long> startTimes = new ConcurrentHashMap<>(); // Lifecycle start: before instantiation @Override public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) { startTimes.put(beanName, System.nanoTime()); return null; // returning null = do not take over, continue the standard flow } // Lifecycle end: after initialization @Override public Object postProcessAfterInitialization(Object bean, String beanName) { Long start = startTimes.remove(beanName); if (start != null) { long costMs = (System.nanoTime() - start) / 1_000_000; if (costMs > 50) { System.out.println("[slow] " + beanName + " created in " + costMs + "ms"); } } return bean; }}- Returning
nullfrompostProcessBeforeInstantiationmeans "I won't interfere; create it normally" — the key difference from "return a proxy and short-circuit" postProcessAfterInitializationis the last link of the lifecycle; by thenbeanis in its final form (possibly already a proxy)- Print only beans over 50ms to avoid flooding; run it once and your startup-optimization checklist appears
Note: this kind of "cross-cutting" logic is AOP in embryo — BeanPostProcessor is the official way Spring hands you to insert into every bean's lifecycle.
| Question | Answer |
|---|---|
Which runs first, @PostConstruct or afterPropertiesSet? | @PostConstruct. The former fires via CommonAnnotationBeanPostProcessor (a BPP) at before-init; the latter is later, in invokeInitMethods |
| Can the constructor see injected dependencies? | Not for field/setter injection — dependencies are not filled yet. Yes for constructor injection, because they arrive as constructor arguments |
@PostConstructprecedesafterPropertiesSetbecause "BPP callbacks" as a whole run before "InitializingBeancallbacks"- To do dependency-related work in a constructor, the only correct approach is constructor injection:
public UserService(UserDao dao)—daois ready before the constructor runs - With
@Autowiredfield injection, accessing the field in the constructor yieldsnull
calling a same-class method annotated with @Transactional inside @PostConstruct (a self-invocation) does not start a transaction. The reason is direct: @PostConstruct runs at "before-init", while the AOP proxy is created only at "after-init" (Section 4). At that moment calls go to the raw object, this.xxx() bypasses the proxy, and the advice chain never gets a chance. The same applies to self-invoked @Async or @Cacheable in @PostConstruct.
to "run once with a transaction or asynchronously at startup", move it into the step-12 ContextRefreshedEvent listener, or inject your own proxy (ObjectProvider<MyService> / @Lazy) so the call goes through the proxy.
The demo below turns the eight stages into an interactive timeline; switch the "prototype" option to see the destroy difference at a glance:
Then switch to the destroy argument to isolate the destroy triple (@PreDestroy → destroy → destroyMethod) and confirm it mirrors the init order exactly.
Knowing the eight stages is not enough — you also need to know who pushes a bean through them. The answer lives in the previous article: step 11 of refresh(), finishBeanFactoryInitialization, walks every non-lazy singleton and drives it down the whole pipeline. This lab spreads the twelve steps out; watch step 11 and see beans pop into existence batch by batch:
Section 8 says "the AOP proxy is born at after-init". Who does the wrapping? The auto-proxy creator — a BeanPostProcessor whose only job is to decide "should this bean be wrapped in a proxy". The lab below unfolds its checkpoint logic: first the candidate check, then the interception inside post-processing:
Finally, back to the population stage itself. How a dependency is injected decides whether the object is still a half-finished shell after step ① — constructor injection is complete at birth, field injection only becomes complete at step ②:
The prototype-does-not-destroy rule from Section 5 is really a scope question. This lab puts all four scopes side by side and counts instances: pick "count the instances" to watch the counter move on every lookup, then "who destroys it" to see the destroy log appear exactly once:
One more special case of "when is it actually born": a FactoryBean definition registers a factory, yet getBean hands you a product. So when does the product exist? Pick "when it really produces" and line it up against getObjectForBeanInstance, the last cell of the debugger in Section 2:
This console is wired to the real container running in your browser; beans, di and query are answered by the kernel on the spot, so every command below tests one claim of this article:
run lab beanscope destroy and lab beanscope probe back to back — the first shows a whole block of destroy log missing, the second shows the instance count still climbing. Every answer in the Section 5 pairing game comes out of these two commands.
Beginners ask the same two questions: "why does my app take 40 seconds to boot?" and "why did @PreDestroy stop printing after I added @Scope("prototype")?" Neither answer is in your code — both come from how scope and init timing are chosen. The sandbox below folds "scope + lazy or not" into one three-position switch; each position shows startup cost, lookup cost and the destroy log side by side:
Startup: 38s# step 11 preInstantiateSingletons news 1240 singletons in one go[slow] dataSource created in 2100ms <- one of the culpritsgetBean(userService): 0.01ms (hit in singletonObjects)On close: @PreDestroy ✔ all fire
the numbers are illustrative, the conclusion is real — a singleton's creation cost lands entirely on the startup moment, a prototype's cost is spread over every lookup. The two failure modes point in opposite directions: "booting takes forever" versus "memory keeps climbing and not one destroy line appears".
A warm-up question, straight from the symmetry table in Section 5:
Now a combined question threading Sections 3, 4, 7 and 8 together:
a bean's life has only eight stages and five callback types. Remember three threads — order: instantiation → population → Aware → before/after init → pooled → destroy; sequence: @PostConstruct before afterPropertiesSet before initMethod, and the same on the destroy side; timing: the AOP proxy is born at "after-init", so any proxy-dependent logic must stay out of @PostConstruct. Etch these three in and lifecycle questions stop being memorization and become reasoning.
Copy every snippet below straight into a search box — do not paraphrase or shorten it. Beginners stall in three places: an annotation that silently does nothing, a null field inside a constructor, and a prototype that never gets destroyed.
| Error text (fragment) | What really happened | 30-second fix | Read more in |
|---|---|---|---|
@PostConstruct is written but prints nothing, with no error either | Since Spring Boot 3 / JDK 9+ the package moved from javax.annotation to jakarta.annotation; you imported the wrong one, so the container simply does not recognise the annotation | Check the import: Boot 3 needs import jakarta.annotation.PostConstruct;, Boot 2 needs javax.annotation.PostConstruct. In IDEA, Alt+Enter to re-import | Section 2 · #16 Hello Spring Boot |
java.lang.NoClassDefFoundError: javax/annotation/PostConstruct | A plain Java SE project or a slim Spring 5 + JDK 11 image lacks the annotation class itself — the type is not on the classpath | Boot 2: add org.apache.tomcat:annotations-api; Boot 3: add jakarta.annotation:jakarta.annotation-api (usually already pulled in by a starter) | Level-2 exercise · #36 Packaging & deploy |
NullPointerException: Cannot invoke "com.example.UserDao.findById(Long)" because "this.dao" is null | You used a field-injected dependency inside the no-arg constructor. The constructor is step 1, population is step 2, so dao is not in yet | Switch to constructor injection: public UserService(UserDao dao) { this.dao = dao; }, or move the logic into @PostConstruct | Section 7 · #6 IoC and DI |
After adding @Scope("prototype"), @PreDestroy / destroy() never print again | The container lets go of a prototype immediately and keeps no reference, so there is nothing to call back — by design, not a bug | Close it yourself: make the prototype implement AutoCloseable and fetch it with try-with-resources, or go back to a stateless singleton | Section 5 · the sandbox above |
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userService': Invocation of init method failed; nested exception is java.lang.IllegalStateException: ... | One of your init callbacks (@PostConstruct / afterPropertiesSet / initMethod) threw, and the container wrapped it as a creation failure | Read only what follows nested exception — the culprit is your own validation, not Spring. A startup validation failure should stop the process | Section 3 · #8 The twelve steps of refresh() |
APPLICATION FAILED TO START plus The dependencies of some of the beans in the application context form a cycle | Two beans wait on each other during creation, and constructor injection cannot offer an early reference | Break one edge along the cycle printed in the log, or inject one side with @Lazy | #10 Circular dependency · #6 IoC and DI |
five of those six lines never appear as a red exception — they show up as silence (no log, no callback). When debugging lifecycle issues, first set logging.level.org.springframework.beans=DEBUG and check whether the container ever reached the relevant stage for that bean. Most "my annotation stopped working" cases are really "it was never registered".
Row five is the only one that shouts, and its culprit sits in a specific cell of the lifecycle. Skip the answer and click the guilty frame:
You add a @PostConstruct that validates configuration. It works on your laptop; the test environment dies on startup with a very long message, and your teammate is already blaming the database.
Goal: with a minimal runnable project, print the whole chain "constructor → Aware → @PostConstruct → afterPropertiesSet → initMethod → the destroy triple", then watch a prototype lose the last three lines.
Step one, a Maven pom.xml that needs only spring-context and the jakarta annotations:
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.1.8</version> </dependency> <dependency> <groupId>jakarta.annotation</groupId> <artifactId>jakarta.annotation-api</artifactId> <version>3.0.0</version> </dependency></dependencies>Step two, the bean (src/main/java/com/example/demo/LifeBean.java):
package com.example.demo;import jakarta.annotation.PostConstruct;import jakarta.annotation.PreDestroy;import org.springframework.beans.factory.BeanNameAware;import org.springframework.beans.factory.DisposableBean;import org.springframework.beans.factory.InitializingBean;import org.springframework.beans.factory.annotation.Value;public class LifeBean implements BeanNameAware, InitializingBean, DisposableBean { @Value("${app.tag:untagged}") private String tag; // field injection: unreadable in the constructor public LifeBean() { System.out.println("1 constructor | tag = " + tag); } @Override public void setBeanName(String name) { System.out.println("2 BeanNameAware | name = " + name); } @PostConstruct public void initAnnotation() { System.out.println("3 @PostConstruct | tag = " + tag); } @Override public void afterPropertiesSet() { System.out.println("4 afterPropertiesSet"); } public void customInit() { System.out.println("5 initMethod"); } @PreDestroy public void preDestroy() { System.out.println("6 @PreDestroy"); } @Override public void destroy() { System.out.println("7 DisposableBean#destroy"); } public void customDestroy() { System.out.println("8 destroyMethod"); }}Step three, the config class and launcher (new Main.java in the same package):
package com.example.demo;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.PropertySource;@Configuration@PropertySource(value = "classpath:app.properties", ignoreResourceNotFound = true)public class Main { @Bean(initMethod = "customInit", destroyMethod = "customDestroy") public LifeBean singletonLife() { return new LifeBean(); } public static void main(String[] args) { try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Main.class)) { System.out.println("---- container ready ----"); } System.out.println("---- container closed ----"); }}Run main. Expected output — every line must match, otherwise your import came from the wrong package:
1 constructor | tag = null2 BeanNameAware | name = singletonLife3 @PostConstruct | tag = untagged4 afterPropertiesSet5 initMethod---- container ready ----6 @PreDestroy7 DisposableBean#destroy8 destroyMethod---- container closed ----Acceptance checklist: ① you can explain why line 1 prints null while line 3 has a value; ② change the @PostConstruct import to a package that does not exist — the app still starts but one line disappears, and you have reproduced "silent failure" yourself; ③ say who invokes lines 6, 7 and 8 respectively.
Change exactly one thing per run and the conclusion flips completely:
- Add
@Scope("prototype")tosingletonLife()(rememberimport org.springframework.context.annotation.Scope;). You will observe lines1-5printing as usual and not a single one of6/7/8; callctx.getBean(LifeBean.class)twice and1-5repeats verbatim. That is the "the container lets go immediately" rule from Section 5 and the comparison figure. - Print
System.out.println(this.hashCode())inside@PostConstruct, then compare it with the identity of whatgetBeanreturns for a class carrying@Transactional. You will observe the two differ — the proxy is pulled on after your init callback ran. - Make
customInit()thrownew IllegalStateException("config validation failed"). You will observe the entire startup aborting withBeanCreationException: Error creating bean with name 'singletonLife': Invocation of init method failed, while lines6/7/8still execute — the "leave no half-built context behind" behaviour from the previous article.
Tip: after variant 1, revisit the sandbox in Section 11, flip the switch to prototype, and confirm both tell exactly the same story.
Write yourself a "lifecycle probe" component so that any future project's slow boot or leaking resource can be measured rather than guessed.
Requirements:
- A
LifecycleProbeimplementingInstantiationAwareBeanPostProcessorthat records each bean's "instantiated at" and "initialization finished at" timestamps - Additionally distinguish three facts: whether
@PostConstructran (inferable from the presence ofCommonAnnotationBeanPostProcessor), and whether the bean got proxied (comparegetClass()of the object returned frompostProcessAfterInitializationwith the one passed in) - On shutdown (via
SmartLifecycleor aContextClosedEventlistener) print a summary table: top 10 slowest beans, how many were proxied, and which lazy beans were never created - Not one line of business bean code may change
Acceptance checklist: ① run it on an empty Boot project and the top 10 includes auto-configured beans such as dataSource; ② mark one bean prototype, fetch it three times, and the table reports three creations; ③ add @Transactional to a bean and it shows up in the "proxied" count; ④ the probe itself costs under 5% extra startup time (verify against the baseline total).
from memory, name the eight stages in order and say which callback sits between "population" and "before-init".
who actually invokes @PostConstruct? Why does it count as "before-init" rather than its own stage?
why is a field-injected value always null inside a constructor while constructor injection is not? How does "complete at birth" differ between the two styles?
why does a prototype bean's destruction never run? If it genuinely holds resources, what are your two legitimate ways out?
at which stage is the AOP proxy created? How does that single moment explain both "@Transactional self-invocation fails inside @PostConstruct" and "getClass() prints a CGLIB name"?
Mantra: **goods in first (instantiation), shelves stocked next (population), a health check before opening (initialization), the badge issued last (proxy) — and prototypes never get a retirement certificate**.