The full eight-step journey from constructor call to shutdown — including the exact step where AOP proxies actually get created, which surprises almost everyone the first time.
Published September 23, 2026
Every Spring bean goes through the same fixed sequence, whether you ever notice it or not. Knowing it precisely is what makes @PostConstruct, BeanPostProcessor, and AOP proxy creation click into place as one coherent story instead of separate magic.
1. Instantiation — the constructor runs, producing a raw object. At this point, dependency-injected fields aren't populated yet (unless they're constructor-injected, in which case they're populated as part of this step).
2. Populate properties — setter/field injection happens here (constructor injection already happened in step 1, as part of construction itself).
3. Aware interfaces called — if the bean implements BeanNameAware, BeanFactoryAware, or ApplicationContextAware, Spring calls the corresponding setter now, giving the bean access to its own name, the factory, or the full context — a way for framework-adjacent code to reach back into the container itself, rarely needed in typical application code.
4. BeanPostProcessor.postProcessBeforeInitialization — every registered BeanPostProcessor in the container gets a chance to inspect or wrap the bean before its own initialization logic runs.
5. Initialization — @PostConstruct-annotated method runs first, then InitializingBean.afterPropertiesSet() (the older, interface-based equivalent) — both exist because @PostConstruct is the modern, generally-preferred choice, while InitializingBean predates it and still appears in older code.
6. BeanPostProcessor.postProcessAfterInitialization — this is where AOP proxies actually get created. Spring's AOP support is itself implemented as a BeanPostProcessor — if a bean matches an AOP pointcut (see Spring AOP), the raw object built in steps 1-5 gets wrapped in a proxy at this exact step, and it's the proxy, not the raw object, that gets returned to whatever requested this bean.
7. Bean ready for use — fully constructed, fully initialized, possibly proxied, injected wherever it's needed.
8. Shutdown (@PreDestroy, then DisposableBean.destroy()) — mirrors step 5's pairing: the modern annotation-based hook runs first, then the legacy interface-based equivalent, both invoked when the container shuts down (e.g. on application context close).
This is the detail most people get wrong on first exposure: AOP proxy creation happens in postProcessAfterInitialization, after @PostConstruct has already run — on the raw, unproxied object. This has a direct, practical consequence: any AOP advice (logging, security checks, @Transactional) that wraps a method will not apply to calls made from within that same bean's own @PostConstruct method, because at that point in the lifecycle, the proxy doesn't exist yet — only the raw object does. This is a variant of the same self-invocation problem covered fully in Spring AOP: proxying only intercepts calls that come in from outside the bean, through the proxy reference — and during @PostConstruct, there is no proxy reference yet at all.
Q: Why does @PostConstruct run before postProcessAfterInitialization instead of after?
A: The ordering is deliberate: @PostConstruct is meant for the bean's own setup logic (validating injected dependencies, initializing internal state) — proxy creation is a cross-cutting concern applied on top of an already-initialized bean, so it makes sense for the bean to finish setting up its own state first, and only then have the framework potentially wrap the finished result.
Q: Does every bean go through step 6's BeanPostProcessor even if it has no matching AOP advice?
A: Yes — every registered BeanPostProcessor runs for every bean, but for a bean with no matching pointcut, the AOP-related post-processor simply returns the bean unchanged (no proxy created) — the step always executes, but proxying is conditional on whether that specific bean actually needs it.
Q: Can a BeanPostProcessor replace the bean entirely, not just wrap it?
A: Yes — postProcessBeforeInitialization/postProcessAfterInitialization both return an Object, and a post-processor is free to return a completely different object than the one it was given (proxying is exactly this: returning a proxy object instead of the original) — this is the extension point AOP itself is built on, not a special-cased framework feature.
Q: What's the practical fix for the self-invocation-during-@PostConstruct problem?
A: Inject the bean's own proxy reference from the ApplicationContext and call through that (awkward and rarely done), or — the more common real fix — restructure so the method needing AOP advice (transactional behavior, for instance) is called from a different bean, ensuring the call comes in through the proxy from outside, exactly the same fix pattern used for Spring AOP's general self-invocation problem.