Spring Bean Lifecycle Interview Questions – Scopes and 3-Level Caching

  • Last Updated: April 28, 2026
  • By: javahandson
  • Series
img

Spring Bean Lifecycle Interview Questions – Scopes and 3-Level Caching

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.

Q1.  What are Spring Beans?

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.

Q2.  What is the bean lifecycle in Spring?

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()");
    }
}

Q3.  How does Spring manage bean lifecycle internally — AbstractBeanFactory, doGetBean(), createBean()?

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

  1. createBeanInstance() — calls the constructor via reflection.
  2. applyMergedBeanDefinitionPostProcessors() — processes @Autowired and @Value metadata.
  3. addSingletonFactory() — registers an ObjectFactory in singletonFactories (level 3 cache) to allow circular dependency resolution.
  4. populateBean() — injects all dependencies.
  5. initializeBean() — runs Aware callbacks, BeanPostProcessor before init, init methods, BeanPostProcessor after init.

Q4.  What are BeanPostProcessors — how and when do they run?

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
    }
}

Q5.  What is AutowiredAnnotationBeanPostProcessor — what does it do?

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:

  • resolve by type first, finding all beans matching the declared type
  • if multiple candidates exist resolve by field name
  • check for @Primary
  • if still ambiguous check @Qualifier
  • if required=true and no match throw NoSuchBeanDefinitionException.
@Autowired
@Qualifier("creditCardPaymentService")
private PaymentService paymentService; // explicitly selects one implementation

Q6.  What is BeanFactoryPostProcessor and how is it different from BeanPostProcessor?

Answer – This is one of the most commonly confused pairs in Spring interviews. The distinction is clean once you understand when each runs.

AspectBeanFactoryPostProcessorBeanPostProcessor
Operates onBeanDefinition (metadata)Bean instances (actual objects)
Runs whenAfter BDs loaded — BEFORE any bean is instantiatedAround each bean’s initialization — AFTER instantiation
Can modifyBeanDefinition metadata and property valuesBean object itself — can replace with proxy
ExamplePropertySourcesPlaceholderConfigurerAutowiredAnnotationBeanPostProcessor

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");
    }
}

Q7.  What are InitializingBean and DisposableBean interfaces?

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");
    }
}

Q8.  @PostConstruct vs afterPropertiesSet() vs init-method — what is the exact execution order?

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.

Q9.  @PreDestroy vs DisposableBean vs destroy-method — what is the execution order?

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.

Q10.  What are all bean scopes — Singleton, Prototype, Request, Session, Application, WebSocket?

Answer – Spring supports six built-in bean scopes, and you can define custom ones. Here is a concise overview:

ScopeInstance PerWeb Only?Destroy Callback?
Singleton (default)Spring containerNoYes
PrototypeEvery getBean() callNoNo
RequestHTTP RequestYesYes
SessionHTTP SessionYesYes
ApplicationServletContextYesYes
WebSocketWebSocket SessionYesYes

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 { }

Q11.  What is the problem with a Prototype bean inside a Singleton — what are all the solutions?

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();
    }
}

Q12.  What is 3-level caching in Spring — singletonObjects, earlySingletonObjects, singletonFactories?

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

  1. Spring starts creating BeanA. createBeanInstance() — BeanA_raw is instantiated.
  2. Spring registers BeanA’s ObjectFactory in singletonFactories (Level 3). The factory, when called, returns either the raw bean or an AOP proxy.
  3. populateBean(BeanA) requires BeanB. Spring starts creating BeanB.
  4. BeanB_raw is instantiated. Spring registers BeanB’s ObjectFactory in Level 3.
  5. populateBean(BeanB) requires BeanA. getSingleton(‘beanA’): Level 1 miss, Level 2 miss, Level 3 HIT.
  6. The factory is called and returns an early reference to BeanA. This reference is moved to earlySingletonObjects (Level 2) and removed from singletonFactories (Level 3).
  7. BeanB receives the early BeanA reference. BeanB finishes initialization and is moved to singletonObjects (Level 1).
  8. Control returns to BeanA. BeanB is now in Level 1 and is injected into BeanA. BeanA finishes and is moved to Level 1.

Q13.  Explain 3-level caching with proxy objects in Spring

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

Q14.  Why does 3-level cache work for Singleton beans but NOT for Prototype beans?

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?

Q15.  Explain 3-level caching with a @Transactional example — trace every cache entry

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.

Leave a Comment