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 toDefaultSingletonBeanRegistry.getSingleton(). - If
mbd.isPrototype()$\to$ CallscreateBean()without caching the instance. - Other scopes (e.g.
request,session, or custom) $\to$ Looks up the registeredScopeimplementation inthis.scopesand executesscope.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:
- The CGLIB
MethodInterceptorintercepts the invocation. - It queries Spring’s
RequestContextHolder.currentRequestAttributes(), which reads the current request’s attributes from aThreadLocalstore. - It retrieves the active
RequestScopebean instance specifically bound to the current HTTP worker thread. - 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
staticinstance field. It enforces strictly one instance per JavaClassLoader. 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 SpringApplicationContextinstances 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 perApplicationContext, cached insingletonObjects). Theprototypescope creates a brand-new instance on every injection orgetBean()call, and Spring never calls@PreDestroyon prototypes. In web applications, Spring addsrequest,session,application, andwebsocketscopes. Injecting a short-lived bean into a singleton causes a Scoped Mismatch (freezing the instance), which is cleanly resolved viaObjectProvider<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
@RequestScopebean directly into a singleton works automatically.” (It freezes the request instance unless wrapped inScopedProxyMode.TARGET_CLASS). - ❌ “To resolve a prototype in a singleton, you should inject
ApplicationContextand callgetBean().” (This is the Service Locator anti-pattern; useObjectProvider<T>or@Lookupinstead). - ❌ “Spring automatically destroys prototype beans when they are garbage collected.” (Spring does not track prototype destruction;
@PreDestroyis 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