Spring Bean Lifecycle Interview Questions – Scopes and 3-Level Caching
-
Last Updated: April 28, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
If you are preparing for a Spring or Spring Boot interview at a product company, mastering the spring bean lifecycle is the single topic that separates average candidates from strong ones. Deep knowledge of how the container manages beans internally — the lifecycle phases, scopes, BeanPostProcessors, and the 3-level caching mechanism for circular dependency resolution — is tested in almost every mid-to-senior level Spring interview today.
This article covers 15 carefully crafted spring bean lifecycle interview questions — ranging from beginner to advanced — along with deep, interview-ready answers, real code examples, and step-by-step traces for complex scenarios. Whether you are a fresher trying to understand what a Spring Bean is, or a senior developer asked to trace the 3-level cache with @Transactional, this article has you covered.
Answer – A Spring Bean is simply a Java object that is managed by the Spring IoC (Inversion of Control) container. Instead of you creating objects yourself using new, you declare them — either via annotations like @Component, @Service, @Repository, @Controller, or via @Bean methods inside @Configuration classes — and the container creates, configures, wires, and manages the lifecycle of those objects for you.
The most important characteristic of a Spring Bean is that the container owns it. This ownership means four things. The container controls when it is created. The container injects dependencies into it. The container calls lifecycle callbacks on it. And the container destroys it when it is no longer needed for scoped beans.
@Component
public class OrderService {
private final PaymentService paymentService;
// Spring injects PaymentService automatically — no new() here
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void placeOrder() {
paymentService.process();
}
}
In this example, OrderService is a Spring Bean. You never wrote new OrderService(…) anywhere. The container read the @Component annotation, created a BeanDefinition for it, saw that it needs a PaymentService, created that bean first, and then injected it into OrderService’s constructor.
Answer – The Spring bean lifecycle is the precise sequence of steps the Spring container follows from the moment it decides to create a bean to the moment it destroys it. Understanding this sequence is critical for both writing correct Spring code and for answering interview questions confidently. Here are all the phases in order.
Phase 1 – BeanDefinition Loading
Before any bean is instantiated, Spring reads your configuration — annotations, XML, or @Bean methods — and builds a BeanDefinition object for each bean. This definition stores metadata: class, scope, constructor arguments, property values, lazy flag, init and destroy method names, and more.
Phase 2 – BeanFactoryPostProcessor Execution
After all BeanDefinitions are loaded but before any bean is instantiated, Spring invokes all BeanFactoryPostProcessor implementations. This is where property placeholder resolution (${db.url}) happens via PropertySourcesPlaceholderConfigurer.
Phase 3 – Bean Instantiation
Spring calls the constructor (or factory method) to create the raw object. No dependencies are injected yet at this moment.
Phase 4 – Dependency Injection / populateBean()
Spring injects all dependencies — via constructor, setter, or field injection (@Autowired). This phase is internally called populateBean().
Phase 5 – Aware Interface Callbacks
If the bean implements certain Aware interfaces, Spring calls them now: BeanNameAware.setBeanName(), BeanFactoryAware.setBeanFactory(), and ApplicationContextAware.setApplicationContext().
Phase 6 – BeanPostProcessor: postProcessBeforeInitialization()
Spring iterates all registered BeanPostProcessor implementations and calls postProcessBeforeInitialization() on each. @PostConstruct methods are triggered here via CommonAnnotationBeanPostProcessor.
Phase 7 – InitializingBean.afterPropertiesSet()
If the bean implements InitializingBean, Spring calls afterPropertiesSet() now.
Phase 8 – Custom init-method
If an init-method is specified via @Bean(initMethod=”…”) or XML, Spring calls it now.
Phase 9 – BeanPostProcessor: postProcessAfterInitialization()
Spring again iterates all BeanPostProcessors and calls postProcessAfterInitialization(). This is where AOP proxies are created — AbstractAutoProxyCreator wraps your bean in a CGLIB proxy at this point.
Phase 10 – Bean Ready for Use
The fully initialized and proxied bean is placed in the singleton cache (singletonObjects) and returned to whoever requested it.
Phase 11 – Destruction (on container shutdown)
When context.close() is called or the JVM shutdown hook triggers: @PreDestroy methods run first, then DisposableBean.destroy(), then the custom destroy-method.
@Component
public class LifecycleDemo implements BeanNameAware, InitializingBean, DisposableBean {
@PostConstruct
public void postConstruct() {
System.out.println("1. @PostConstruct");
}
@Override
public void setBeanName(String name) {
System.out.println("Aware: setBeanName = " + name);
}
@Override
public void afterPropertiesSet() {
System.out.println("2. afterPropertiesSet()");
}
@PreDestroy
public void preDestroy() {
System.out.println("3. @PreDestroy");
}
@Override
public void destroy() {
System.out.println("4. DisposableBean.destroy()");
}
}
Answer – When you call applicationContext.getBean(“orderService”) — or when Spring injects a dependency — the call chain internally flows through several key classes. Understanding this chain is what separates deep Spring knowledge from surface-level knowledge.
AbstractBeanFactory.doGetBean()
This is the most important method in the entire Spring container. It first checks the singleton cache using getSingleton(beanName) across all 3 levels. If the bean is found and fully initialized it is returned immediately. If not, it checks whether the bean is Prototype or scoped. For singletons it calls getSingleton(beanName, ObjectFactory) which internally calls createBean(). It also handles parent BeanFactory delegation and circular dependency detection structures.
// Simplified call chain inside doGetBean() doGetBean() |-- getSingleton() -> check singletonObjects (level 1) | |-- hit -> return bean | +-- miss -> check earlySingletonObjects (level 2) | |-- hit -> return early reference | +-- miss -> check singletonFactories (level 3) | |-- hit -> call factory, move to L2 | +-- miss -> proceed to createBean() +-- createBean() -> instantiateBean() -> populateBean() -> initializeBean()
AbstractAutowireCapableBeanFactory.createBean()
This method handles the actual creation. It first calls resolveBeforeInstantiation() giving InstantiationAwareBeanPostProcessors a chance to return a proxy instead of the real bean. Then it calls doCreateBean().
doCreateBean() — Key Steps
Answer – A BeanPostProcessor is one of the most powerful extension points in Spring. It lets you intercept every bean after it is instantiated but around the initialization phase, and either modify it or replace it entirely with a different object — like a proxy.
The interface defines two methods. postProcessBeforeInitialization() is called after dependency injection but before @PostConstruct and afterPropertiesSet(). postProcessAfterInitialization() is called after all init methods complete — and this is where AOP proxies are created.
public interface BeanPostProcessor {
// Called BEFORE init methods (@PostConstruct, afterPropertiesSet, init-method)
default Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}
// Called AFTER init methods — AOP proxies are created HERE
default Object postProcessAfterInitialization(Object bean, String beanName) {
return bean;
}
}
Built-in BeanPostProcessors you must know for interviews:
CommonAnnotationBeanPostProcessor — processes @PostConstruct, @PreDestroy, @Resource
AutowiredAnnotationBeanPostProcessor — processes @Autowired, @Value, @Inject
AbstractAutoProxyCreator — creates AOP proxies for @Transactional, @Async, and other AOP advice
An important note: BeanPostProcessors themselves are NOT processed by other BeanPostProcessors to avoid chicken-and-egg problems. They are instantiated and registered very early in container startup, before any regular beans are created.
@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("After init: " + beanName
+ " | Class: " + bean.getClass().getSimpleName());
return bean; // always return bean or a replacement object
}
}
Answer – AutowiredAnnotationBeanPostProcessor is the BeanPostProcessor responsible for processing @Autowired, @Value, and @Inject annotations in your Spring beans. It is one of the most important internal processors in the framework and is automatically registered when you use @SpringBootApplication.
Step 1 – Metadata Collection
During doCreateBean(), Spring calls applyMergedBeanDefinitionPostProcessors(). AutowiredAnnotationBeanPostProcessor implements MergedBeanDefinitionPostProcessor so it runs here. It scans the bean’s class for fields, methods, and constructors annotated with @Autowired or @Value and builds injection metadata — essentially a list of things to inject.
Step 2 – Injection via postProcessProperties()
During populateBean(), Spring calls postProcessProperties() on all InstantiationAwareBeanPostProcessors. AutowiredAnnotationBeanPostProcessor implements this interface. It uses the collected metadata to perform the actual injection: for fields via reflection using field.set(bean, resolvedValue), for methods by calling the method with resolved arguments, and for constructors during instantiation itself.
@Service
public class OrderService {
@Autowired
private PaymentService paymentService; // processed by AutowiredAnnotationBeanPostProcessor
@Value("${order.timeout:30}")
private int timeoutSeconds; // @Value also processed here
}
The @Autowired resolution logic follows these steps:
@Autowired
@Qualifier("creditCardPaymentService")
private PaymentService paymentService; // explicitly selects one implementation
Answer – This is one of the most commonly confused pairs in Spring interviews. The distinction is clean once you understand when each runs.
| Aspect | BeanFactoryPostProcessor | BeanPostProcessor |
| Operates on | BeanDefinition (metadata) | Bean instances (actual objects) |
| Runs when | After BDs loaded — BEFORE any bean is instantiated | Around each bean’s initialization — AFTER instantiation |
| Can modify | BeanDefinition metadata and property values | Bean object itself — can replace with proxy |
| Example | PropertySourcesPlaceholderConfigurer | AutowiredAnnotationBeanPostProcessor |
BeanFactoryPostProcessor runs after all BeanDefinitions are loaded but before any beans are created. You have access to the BeanFactory and can read and modify any BeanDefinition. The most famous built-in implementation is PropertySourcesPlaceholderConfigurer which resolves ${property.key} placeholders in @Value annotations and XML before any bean is created.
@Component
public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
BeanDefinition bd = beanFactory.getBeanDefinition("orderService");
// Can modify scope, lazy flag, or property values before any instance is created
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
System.out.println("Changed orderService scope to prototype");
}
}
Answer – InitializingBean and DisposableBean are two callback interfaces provided by Spring that allow a bean to hook into its own lifecycle — specifically after dependencies are set and before the container destroys it.
InitializingBean declares a single method afterPropertiesSet() which Spring calls after all properties have been set on the bean, after populateBean() and after @PostConstruct has run. It is the programmatic alternative to @PostConstruct.
DisposableBean declares destroy() which Spring calls when the container is shutting down, after @PreDestroy has run. Both interfaces couple your code to the Spring API, so the Spring team recommends using @PostConstruct and @PreDestroy instead for application code.
@Component
public class DatabaseConnectionPool implements InitializingBean, DisposableBean {
private Connection connection;
@Override
public void afterPropertiesSet() throws Exception {
// Called after all @Autowired fields are set
this.connection = DriverManager.getConnection(
"jdbc:mysql://localhost/mydb", "user", "pass");
System.out.println("Connection pool initialized");
}
@Override
public void destroy() throws Exception {
if (connection != null && !connection.isClosed()) {
connection.close();
}
System.out.println("Connection pool closed");
}
}
Answer – This question is asked very frequently in Spring interviews. The execution order is fixed and deterministic.
| 📌 Exact init order: (1) @PostConstruct → (2) afterPropertiesSet() → (3) custom init-method |
@Component
public class InitOrderDemo implements InitializingBean {
@PostConstruct
public void postConstruct() {
System.out.println("1. @PostConstruct runs first");
}
@Override
public void afterPropertiesSet() {
System.out.println("2. afterPropertiesSet() runs second");
}
public void customInit() {
System.out.println("3. init-method runs third");
}
}
// In @Configuration:
@Bean(initMethod = "customInit")
public InitOrderDemo initOrderDemo() {
return new InitOrderDemo();
}
Why this order? @PostConstruct is processed by CommonAnnotationBeanPostProcessor inside postProcessBeforeInitialization(). This runs before Spring calls afterPropertiesSet() and the custom init-method, both of which are invoked inside initializeBean() in this sequence: first applyBeanPostProcessorsBeforeInitialization() triggers @PostConstruct, then invokeInitMethods() calls afterPropertiesSet() first and the custom init-method second.
Answer – Similar to the init order, the destroy order is also fixed. On container shutdown:
| 📌 Exact destroy order: (1) @PreDestroy → (2) DisposableBean.destroy() → (3) custom destroy-method |
@Component
public class DestroyOrderDemo implements DisposableBean {
@PreDestroy
public void preDestroy() {
System.out.println("1. @PreDestroy runs first");
}
@Override
public void destroy() {
System.out.println("2. DisposableBean.destroy() runs second");
}
public void customDestroy() {
System.out.println("3. destroy-method runs third");
}
}
@Bean(destroyMethod = "customDestroy")
public DestroyOrderDemo destroyOrderDemo() {
return new DestroyOrderDemo();
}
Important caveats
Prototype beans do NOT get destroy callbacks. The container gives up ownership after handing off the prototype — from that point the caller is responsible for cleanup.
In standalone apps you need context.registerShutdownHook() for destruction callbacks to fire. Spring Boot handles this automatically.
@Bean’s destroyMethod defaults to ‘(inferred)’ which means Spring auto-detects and calls close() or shutdown() methods. This is why DataSource and other closeable resources are properly closed in Spring Boot without explicit configuration.
Answer – Spring supports six built-in bean scopes, and you can define custom ones. Here is a concise overview:
| Scope | Instance Per | Web Only? | Destroy Callback? |
| Singleton (default) | Spring container | No | Yes |
| Prototype | Every getBean() call | No | No |
| Request | HTTP Request | Yes | Yes |
| Session | HTTP Session | Yes | Yes |
| Application | ServletContext | Yes | Yes |
| WebSocket | WebSocket Session | Yes | Yes |
The singleton scope is the default. One instance is created per Spring container and all calls to getBean() return the same object. Prototype creates a new instance every time the bean is requested — there is no cache and destroy callbacks are not called.
The web scopes (Request, Session, Application, WebSocket) are only valid in a web-aware ApplicationContext. When injecting a short-lived scoped bean into a Singleton you must use proxyMode = ScopedProxyMode.TARGET_CLASS. Without it, Spring would try to inject the short-lived bean at container startup which makes no sense.
// Request scope — proxyMode required when injecting into Singleton
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext { }
// Session scope
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart { }
Answer – This is called the prototype bean scoping problem, or singleton absorbing prototype. When Spring creates a Singleton bean and injects a Prototype dependency into it, Spring does the injection exactly once — at startup, when the Singleton is created. After that, the Singleton holds a reference to that single prototype instance forever. You never get a new prototype instance, defeating the entire purpose of prototype scope.
| @Component @Scope(“prototype”) public class TaskProcessor { // Should be a fresh instance per use } @Service public class TaskService { @Autowired private TaskProcessor taskProcessor; // PROBLEM: injected once, reused forever public void execute() { taskProcessor.process(); // always the SAME instance } } |
Solution 1 — @Lookup Method Injection (Recommended for abstract services)
Spring overrides the annotated abstract method at runtime using CGLIB to return a new prototype instance each time. This is clean and does not couple to ApplicationContext.
@Service
public abstract class TaskService {
@Lookup
public abstract TaskProcessor createProcessor(); // Spring overrides this via CGLIB
public void execute() {
TaskProcessor processor = createProcessor(); // fresh prototype each time
processor.process();
}
}
Solution 2 — ObjectProvider<T> (Cleanest for most cases)
@Service
public class TaskService {
@Autowired
private ObjectProvider<TaskProcessor> processorProvider;
public void execute() {
TaskProcessor processor = processorProvider.getObject(); // fresh each time
processor.process();
}
}
Solution 3 — Scoped Proxy
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TaskProcessor { }
// The Singleton holds a proxy — every method call through the proxy
// routes through fresh prototype creation logic
Solution 4 — ApplicationContext.getBean() (Anti-pattern)
@Service
public class TaskService implements ApplicationContextAware {
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext ctx) {
this.context = ctx;
}
public void execute() {
TaskProcessor processor = context.getBean(TaskProcessor.class); // fresh
processor.process();
}
}
Answer – Spring’s 3-level caching is the mechanism that allows it to resolve circular dependencies between singleton beans. Without it, consider: BeanA depends on BeanB and BeanB depends on BeanA. If Spring tries to fully create BeanA before BeanB, and fully create BeanB before returning to finish BeanA, it hits infinite recursion. The 3-level cache breaks this cycle by providing a way to hand out an early reference to a partially constructed bean.
All three caches live in DefaultSingletonBeanRegistry:
// Level 1: Fully initialized beans — the main cache used by getBean()
private final Map<String, Object> singletonObjects
= new ConcurrentHashMap<>(256);
// Level 2: Early exposed bean references (partially initialized, proxied if needed)
private final Map<String, Object> earlySingletonObjects
= new ConcurrentHashMap<>(16);
// Level 3: ObjectFactory instances that produce an early reference
// The factory may return the raw bean or an AOP proxy of it
private final Map<String, ObjectFactory<?>> singletonFactories
= new HashMap<>(16);
How it works — BeanA depends on BeanB, BeanB depends on BeanA
Answer – When AOP is involved — for example when BeanA is @Transactional — the Level 3 factory does more than return the raw bean. It calls getEarlyBeanReference() which gives AbstractAutoProxyCreator a chance to create the CGLIB proxy before the bean is fully initialized.
| // From doCreateBean() — this is registered in Level 3 BEFORE populateBean() runs addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); // getEarlyBeanReference() iterates SmartInstantiationAwareBeanPostProcessors. // AbstractAutoProxyCreator creates the CGLIB proxy here if the bean needs proxying. |
The earlyProxyReferences safety mechanism
When BeanB requests the early reference to @Transactional BeanA, AbstractAutoProxyCreator creates OS_proxy and stores it in its own earlyProxyReferences map. Later, when BeanA’s own postProcessAfterInitialization() is called, AbstractAutoProxyCreator checks earlyProxyReferences — sees the proxy was already created for this bean — and returns the same proxy object instead of creating a new one. This guarantees that the proxy object in Level 1 (singletonObjects) is the same object as the one BeanB received as an early reference. Consistency is preserved.
// Scenario: @Transactional BeanA depends on BeanB; BeanB depends on BeanA
// Step 1: BeanA_raw created. factory(BeanA) registered in L3.
// Step 2: populateBean(BeanA) needs BeanB. BeanB_raw created. factory(BeanB) in L3.
// Step 3: populateBean(BeanB) needs BeanA.
// L1 miss -> L2 miss -> L3 HIT
// factory() called -> AbstractAutoProxyCreator creates BeanA_proxy
// BeanA_proxy stored in L2. Factory removed from L3.
// Step 4: BeanB.beanA = BeanA_proxy (correct — transactional proxy injected)
// Step 5: BeanB finishes -> BeanB_proxy created in postProcessAfterInitialization
// BeanB_proxy -> L1
// Step 6: BeanA finishes. postProcessAfterInitialization called.
// AbstractAutoProxyCreator checks earlyProxyReferences.
// BeanA_proxy already created! Returns existing proxy.
// BeanA_proxy -> L1.
// Final: L1 = { beanA: BeanA_proxy, beanB: BeanB_proxy }
// BeanB.beanA = BeanA_proxy (same object as L1) — consistent
Answer – The 3-level cache is designed for singleton beans only. Spring explicitly does NOT allow circular dependencies for prototype-scoped beans.
Why the caching strategy requires singletons
The caching strategy works because there is exactly one instance of each singleton bean in the container. When Spring registers the ObjectFactory in Level 3, it is making a promise: ‘here is a way to get a reference to the one and only BeanA.’ Because it is a singleton, the same raw object that was registered in Level 3 is the same object that eventually completes initialization and lands in Level 1.
For prototypes there is no concept of the one and only instance. Every getBean() creates a new object. If ProtoBeanA and ProtoBeanB both need each other and are prototype-scoped, Spring would need to create a new ProtoBeanA, then to inject ProtoBeanB create a new ProtoBeanB, then to inject ProtoBeanA into ProtoBeanB create another new ProtoBeanA, and so on infinitely. There is no stable reference to put in a cache.
Spring’s behaviour with prototype circular dependencies
Spring tracks prototype beans currently in creation using a Set<String> called prototypesCurrentlyInCreation. If it detects that a prototype bean is being requested while that same bean is already in creation — indicating a cycle — it throws BeanCurrentlyInCreationException.
@Component
@Scope("prototype")
public class ProtoA {
@Autowired private ProtoB protoB; // circular dependency
}
@Component
@Scope("prototype")
public class ProtoB {
@Autowired private ProtoA protoA; // triggers BeanCurrentlyInCreationException
}
// Spring throws at startup:
// BeanCurrentlyInCreationException:
// Requested bean is currently in creation:
// Is there an unresolvable circular reference?
Answer – This is the most advanced tracing question you will encounter in a Spring interview. Let’s trace every cache entry step by step.
Scenario
@Service
@Transactional
public class OrderService {
@Autowired private InventoryService inventoryService;
}
@Service
@Transactional
public class InventoryService {
@Autowired private OrderService orderService; // circular dependency
}
Step-by-step trace with cache states
Step 1: Spring starts creating OrderService. createBeanInstance() creates OS_raw. addSingletonFactory() registers a factory in Level 3. Cache state: L3 = { orderService: factory(OS_raw) }, L2 = {}, L1 = {}.
Step 2: populateBean(OrderService) needs InventoryService. Spring starts creating InventoryService. IS_raw created. Its factory registered in Level 3. Cache state: L3 = { orderService: factory, inventoryService: factory(IS_raw) }.
Step 3: populateBean(InventoryService) needs OrderService. getSingleton(‘orderService’): L1 miss, L2 miss, L3 HIT. Factory called — getEarlyBeanReference() runs. AbstractAutoProxyCreator creates OS_proxy. OS_proxy stored in L2. Factory removed from L3. Cache state: L3 = { inventoryService: factory }, L2 = { orderService: OS_proxy }, L1 = {}. InventoryService.orderService = OS_proxy.
Step 4: InventoryService completes populateBean(). initializeBean() runs. postProcessAfterInitialization() — AbstractAutoProxyCreator creates IS_proxy (no entry in earlyProxyReferences for inventoryService). IS_proxy placed in Level 1. Cache state: L1 = { inventoryService: IS_proxy }.
Step 5: Control returns to OrderService.populateBean(). InventoryService found in Level 1 as IS_proxy. OrderService.inventoryService = IS_proxy.
Step 6: OrderService completes populateBean(). initializeBean() runs. postProcessAfterInitialization() called. AbstractAutoProxyCreator checks earlyProxyReferences — OS_proxy was already created in Step 3! Returns existing OS_proxy instead of creating a new one. OS_proxy moved to Level 1. Level 2 entry for orderService cleared. Final cache state: L1 = { inventoryService: IS_proxy, orderService: OS_proxy }, L2 = {}, L3 = {}.
Final state verification
OrderService in container = OS_proxy (CGLIB proxy with @Transactional advice)
InventoryService in container = IS_proxy (CGLIB proxy with @Transactional advice)
InventoryService.orderService = OS_proxy — same object as in Level 1. Consistent.
OrderService.inventoryService = IS_proxy — same object as in Level 1. Consistent.