🍃 Section 5 · Question #10

What is Bean scope

In the Spring Framework, a Bean Scope defines the lifecycle boundary, instantiation strategy, visibility, and total instance count of an object created from a BeanDefinition wit...


🟢 Junior Level

1. What is a Bean Scope?

In the Spring Framework, a Bean Scope defines the lifecycle boundary, instantiation strategy, visibility, and total instance count of an object created from a BeanDefinition within the IoC container.

When a class is declared as a @Component or a method as @Bean, the container must know whether to maintain a single shared instance for the entire application or generate a brand-new object on every request.


2. The Two Core Scopes

By default, every bean in Spring has singleton scope:

Scope Description Number of Instances in Memory
singleton (Default) Exactly one shared instance per ApplicationContext 1 instance per container
prototype A new instance is created every time the bean is requested $N$ instances (unlimited)
package com.example.demo;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

// 1. Singleton Scope (Default - @Scope can be omitted)
@Service
public class OrderService {
    // Shared across all threads and user requests in the application
}

// 2. Prototype Scope (Creates a fresh object on every getBean() or injection)
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class InvoiceReportTask {
    // Carries state specific to an individual report generation run
}
// Testing instance identity in code:
OrderService s1 = context.getBean(OrderService.class);
OrderService s2 = context.getBean(OrderService.class);
System.out.println(s1 == s2); // true: Both point to the exact same reference in the JVM heap

InvoiceReportTask t1 = context.getBean(InvoiceReportTask.class);
InvoiceReportTask t2 = context.getBean(InvoiceReportTask.class);
System.out.println(t1 == t2); // false: Two completely distinct object instances in heap

🟡 Middle Level

1. The Architectural Scope SPI Interface

Spring’s scoping mechanism is built on the org.springframework.beans.factory.config.Scope interface:

package org.springframework.beans.factory.config;

public interface Scope {

    // Retrieve existing bean or create a new instance via objectFactory
    Object get(String name, ObjectFactory<?> objectFactory);

    // Remove bean from the underlying scope (e.g., when HTTP session expires)
    Object remove(String name);

    // Register callback executed when the scope is destroyed
    void registerDestructionCallback(String name, Runnable callback);

    // Resolve contextual objects (e.g., HttpServletRequest)
    Object resolveContextualObject(String key);

    // Conversation ID for debugging/tracking (e.g., Session ID)
    String getConversationId();
}

When client code calls context.getBean(beanName), AbstractBeanFactory.doGetBean() inspects the bean’s scope:

  • If mbd.isSingleton() $\to$ Delegates to DefaultSingletonBeanRegistry.getSingleton().
  • If mbd.isPrototype() $\to$ Calls createBean() without caching the instance.
  • Other scopes (e.g. request, session, or custom) $\to$ Looks up the registered Scope implementation in this.scopes and executes scope.get(beanName, () -> createBean(...)).

2. The Scoped Mismatch Dilemma

The most insidious bug when working with scopes is injecting a short-lived bean (prototype, request) directly into a long-lived bean (singleton):

@Service // Singleton (Instantiated once during application startup)
public class OrderProcessor {

    @Autowired // ❌ ARCHITECTURAL BUG: Injected ONLY ONCE at startup!
    private PaymentSession paymentSession; // Prototype or Request Scoped

    public void processOrder() {
        // paymentSession remains the exact same frozen instance for all requests!
        paymentSession.pay(); 
    }
}

Why This Occurs:

The singleton bean OrderProcessor is created and wired only once when the container boots. Spring resolves all @Autowired dependencies at that moment. The short-lived PaymentSession is created once and permanently captured inside the singleton field. The prototype scope is effectively negated, creating severe multi-user concurrency bugs and data cross-contamination!


🔴 Senior Level

1. The 4 Solutions to Scoped Mismatch

Solution 1: ObjectProvider<T> (Modern, Idiomatic, & Type-Safe)

Spring provides ObjectProvider<T> to fetch fresh instances lazily on demand without injecting the heavy ApplicationContext:

@Service
public class OrderProcessor {

    private final ObjectProvider<PaymentSession> sessionProvider;

    public OrderProcessor(ObjectProvider<PaymentSession> sessionProvider) {
        this.sessionProvider = sessionProvider;
    }

    public void processOrder() {
        // Obtains a brand-new prototype or request instance on every execution:
        PaymentSession session = sessionProvider.getObject();
        session.pay();
    }
}

Solution 2: Scoped Proxy (ScopedProxyMode.TARGET_CLASS)

Spring injects a dynamic CGLIB proxy into the singleton. The proxy intercepts method calls and dynamically delegates them to the active instance associated with the current execution thread:

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserRequestContext {
    private String userId;
    // Getters and setters
}

@Service
public class BillingService {
    // Injected with a CGLIB dynamic proxy wrapper of UserRequestContext:
    private final UserRequestContext userContext;

    public BillingService(UserRequestContext userContext) {
        this.userContext = userContext;
    }

    public void bill() {
        // userContext.getUserId() transparently looks up the active RequestContextHolder
        // (ThreadLocal) for the current HTTP worker thread!
        System.out.println("Billing user: " + userContext.getUserId());
    }
}

Solution 3: Method Injection (@Lookup)

Spring uses CGLIB at startup to generate a subclass overriding an abstract or concrete method, dynamically delegating calls to BeanFactory.getBean(Class):

@Service
public abstract class ReportService {

    public void generate() {
        ReportTask task = getReportTask(); // CGLIB overrides this method at bytecode level
        task.execute();
    }

    @Lookup
    protected abstract ReportTask getReportTask();
}

Solution 4: Provider<T> (JSR-330 / Jakarta Inject)

Standard Jakarta EE provider interface, operating identically to ObjectProvider:

@Service
public class OrderProcessor {
    @Autowired
    private Provider<PaymentSession> sessionProvider;
}

2. Custom Scope Implementation: ThreadScope

Spring allows building custom domain scopes (e.g., tenant-based multitenancy, batch transaction scope, or custom thread-bound scopes):

package com.example.scope;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;

import java.util.HashMap;
import java.util.Map;

public class CustomThreadScope implements Scope {

    private final ThreadLocal<Map<String, Object>> threadScope = 
            ThreadLocal.withInitial(HashMap::new);

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Map<String, Object> scope = threadScope.get();
        return scope.computeIfAbsent(name, k -> objectFactory.getObject());
    }

    @Override
    public Object remove(String name) {
        return threadScope.get().remove(name);
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        // Destruction handling for thread cleanup
    }

    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Override
    public String getConversationId() {
        return Thread.currentThread().getName();
    }

    // Critical for thread pool worker hygiene to avoid memory leaks:
    public void clear() {
        threadScope.remove(); 
    }
}

Registering the Custom Scope:

@Configuration
public class ScopeConfig {

    @Bean
    public static CustomScopeConfigurer customScopeConfigurer() {
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        configurer.addScope("thread", new CustomThreadScope());
        return configurer;
    }
}

4 Tricky Questions

1. What happens if you inject a @Scope("prototype") bean into a @Scope("singleton") bean via standard constructor or @Autowired?

Answer: The prototype bean is instantiated only once during application startup when the singleton bean is constructed.

Because dependencies are resolved and assigned during the singleton’s property population phase, that initial prototype instance remains permanently captured in the singleton’s field. On all subsequent calls to the singleton, the exact same prototype instance is reused. The prototype scope is effectively nullified, leading to thread-safety issues and stale state bugs if the prototype bean is stateful.


2. How does ScopedProxyMode.TARGET_CLASS work under the hood when a @RequestScope bean is injected into a singleton?

Answer: Spring generates a dynamic CGLIB proxy subclass of the target class. This proxy is injected into the singleton bean at application startup.

When the singleton invokes any method on this proxy:

  1. The CGLIB MethodInterceptor intercepts the invocation.
  2. It queries Spring’s RequestContextHolder.currentRequestAttributes(), which reads the current request’s attributes from a ThreadLocal store.
  3. It retrieves the active RequestScope bean instance specifically bound to the current HTTP worker thread.
  4. It delegates the method call to that thread’s active bean instance.

This decouples the singleton’s long lifecycle from the request’s short lifecycle.


3. What is the fundamental difference between the Gang of Four (GoF) Singleton design pattern and a Spring Singleton?

Answer:

  • GoF Singleton: Hardcodes the singleton invariant into the class implementation using a private constructor and a static instance field. It enforces strictly one instance per Java ClassLoader. It is notoriously difficult to mock and unit-test.
  • Spring Singleton: The class itself is a standard, unconstrained POJO with a public constructor. The singleton invariant is enforced by the Spring IoC Container (ApplicationContext), which ensures one instance per container definition. You can have multiple distinct Spring ApplicationContext instances in the same JVM, each containing its own isolated singleton instance of that class.

4. What is the critical memory leak hazard when implementing a custom ThreadScope backed by ThreadLocal?

Answer: Modern application servers (such as Tomcat or Jetty) and async task executors use thread pools where worker threads are reused across thousands of requests rather than terminated.

If a custom ThreadScope stores bean instances in a ThreadLocal<Map<String, Object>> without explicitly calling threadLocal.remove() at the end of each task or HTTP request, the worker thread retains strong references to those beans permanently in heap memory. This results in thread pool memory leaks (Metaspace / Heap leak) and causes data contamination where subsequent requests handled by the same thread inadvertently access leftover state from previous requests.


🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“A Spring Bean Scope defines the lifecycle and instance cardinality of a bean. The default is singleton (exactly 1 instance per ApplicationContext, cached in singletonObjects). The prototype scope creates a brand-new instance on every injection or getBean() call, and Spring never calls @PreDestroy on prototypes. In web applications, Spring adds request, session, application, and websocket scopes. Injecting a short-lived bean into a singleton causes a Scoped Mismatch (freezing the instance), which is cleanly resolved via ObjectProvider<T> or a Scoped Proxy (ScopedProxyMode.TARGET_CLASS).”


Red Flags & Pitfalls (DO NOT Say)

  • ❌ “A Spring singleton is identical to a GoF Singleton.” (GoF is 1 instance per ClassLoader; Spring is 1 instance per ApplicationContext).
  • ❌ “Injecting a @RequestScope bean directly into a singleton works automatically.” (It freezes the request instance unless wrapped in ScopedProxyMode.TARGET_CLASS).
  • ❌ “To resolve a prototype in a singleton, you should inject ApplicationContext and call getBean().” (This is the Service Locator anti-pattern; use ObjectProvider<T> or @Lookup instead).
  • ❌ “Spring automatically destroys prototype beans when they are garbage collected.” (Spring does not track prototype destruction; @PreDestroy is never called).

Scope Resolution Decision Tree

Bean Scope Selection Matrix:
├─ Is the bean stateless business logic or shared infrastructure?
│  └─ YES ──> Use singleton (Default)
├─ Does the bean hold per-operation mutable state or calculation parameters?
│  └─ YES ──> Use prototype
│             └─ Injected into a singleton? ──> Use ObjectProvider<T> or @Lookup
└─ Is the bean tied to web request / user session context?
   ├─ Per HTTP request? ──> @RequestScope (proxyMode = TARGET_CLASS)
   ├─ Per user session? ──> @SessionScope (proxyMode = TARGET_CLASS)
   └─ Custom context?   ──> Custom Scope via Scope SPI + CustomScopeConfigurer