Definition
- Singleton scope (default) -- one instance per
ApplicationContext, shared by all injection points and all request threads; eager initialization by default. - Prototype scope -- a new instance created every time the bean is requested; Spring does not manage destruction, so
@PreDestroynever runs. - Request scope -- one instance per HTTP request; destroyed automatically after the response is sent.
- Session scope -- one instance per HTTP session; destroyed when the session expires or is invalidated.
- Prototype-into-singleton trap -- injecting a prototype or request-scoped bean into a singleton gives the singleton ONE instance forever because construction happens only once.
- ObjectProvider
-- the preferred fix: inject a factory that produces a fresh instance on every getObject()call.
At National Bank of India, the main vault is a singleton: one vault shared by all tellers. A receipt slip is a prototype: a fresh slip for every transaction. A token number is request-scoped: one per customer visit, discarded after the visit. The trap: if the branch manager grabs one receipt slip at the morning meeting and hands it to every customer all day, every customer gets the same receipt number.
Understanding the Concepts
What are all the Spring bean scopes?
Spring provides six built-in scopes. The first two work in any application. The remaining four require a web-aware ApplicationContext.
| Scope | Instances | Lifecycle | Destruction |
|---|---|---|---|
| singleton | One per ApplicationContext | Created at startup (eager) | @PreDestroy on shutdown |
| prototype | New per getBean/injection | Created on demand | NOT managed by Spring |
| request | One per HTTP request | Created on first access | Destroyed after response |
| session | One per HTTP session | Created on first access | Destroyed on session expiry |
| application | One per ServletContext | Created on first access | Destroyed on context shutdown |
| websocket | One per WebSocket session | Created on first access | Destroyed on session close |
package com.bank.service;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.UUID;
// SINGLETON (default) -- one instance, shared by all
@Component
public class InterestRateService {
// All threads share this instance
}
// PROTOTYPE -- new instance every time
@Component
@Scope("prototype")
public class PaymentTransaction {
private final String transactionId = UUID.randomUUID().toString();
public String getTransactionId() {
return transactionId;
}
}
package com.bank;
import com.bank.service.PaymentTransaction;
import org.springframework.context.ApplicationContext;
public class ScopeDemo {
public static void main(String[] args) {
ApplicationContext ctx = // ...
PaymentTransaction t1 = ctx.getBean(PaymentTransaction.class);
PaymentTransaction t2 = ctx.getBean(PaymentTransaction.class);
System.out.println(t1.getTransactionId()); // e.g., a3f2-...
System.out.println(t2.getTransactionId()); // e.g., b7c4-...
System.out.println(t1 == t2); // false -- different instances
}
}
a3f2e4d1-1234-4567-89ab-cdef01234567
b7c4a8e2-9876-5432-10fe-dcba98765432
false
What is the difference between singleton and prototype?
| Aspect | Singleton | Prototype |
|---|---|---|
| Instances | One per container | New per request |
| Initialization | Eager (at startup) | Lazy (on demand) |
| Thread safety | Your responsibility | Not shared, inherently safe |
| @PreDestroy | Runs on shutdown | Never runs |
| Memory | One object in heap | Many objects, GC reclaims |
| Use case | Stateless services | Stateful per-operation objects |
package com.bank.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
@Service // singleton by default
public class TransferService {
@Autowired
private ApplicationContext ctx;
public void transfer(String from, String to, java.math.BigDecimal amount) {
// Get a fresh prototype for each transfer
PaymentTransaction tx = ctx.getBean(PaymentTransaction.class);
System.out.println("Transaction: " + tx.getTransactionId());
// Each call gets a NEW PaymentTransaction
}
}
What is the prototype-into-singleton trap?
When you inject a prototype-scoped bean directly into a singleton, the singleton's constructor runs ONCE. The prototype is resolved ONCE during that construction and stored in a final field forever. Every subsequent use of the singleton sees the same prototype instance, defeating the purpose of prototype scope.
WRONG -- direct injection of prototype into singleton:
package com.bank.service;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class PaymentProcessingService {
private final PaymentTransaction transaction; // injected ONCE
public PaymentProcessingService(PaymentTransaction transaction) {
this.transaction = transaction; // this ONE instance is stored forever
}
public void processPayment(String accountId, BigDecimal amount) {
// WRONG: every payment uses the SAME transaction object
System.out.println("Processing with TX: " + transaction.getTransactionId());
// Always prints the same ID!
}
}
processPayment("SAV2001", 10000) -> Processing with TX: a3f2-...
processPayment("SAV2002", 20000) -> Processing with TX: a3f2-... // SAME ID!
processPayment("SAV2003", 30000) -> Processing with TX: a3f2-... // SAME ID!
How do you fix the prototype-into-singleton trap?
There are three fixes, from most preferred to least:
Fix 1: ObjectProvider (PREFERRED)
ObjectProvider is a lazy, factory-like wrapper. Call getObject() each time you need a fresh prototype. This is the cleanest and most explicit approach.
package com.bank.service;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class PaymentProcessingService {
private final ObjectProvider<PaymentTransaction> transactionProvider;
public PaymentProcessingService(
ObjectProvider<PaymentTransaction> transactionProvider) {
this.transactionProvider = transactionProvider;
}
public void processPayment(String accountId, BigDecimal amount) {
// getObject() creates a FRESH prototype each time
PaymentTransaction tx = transactionProvider.getObject();
System.out.println("Processing with TX: " + tx.getTransactionId());
}
}
processPayment("SAV2001", 10000) -> Processing with TX: a3f2-...
processPayment("SAV2002", 20000) -> Processing with TX: b7c4-... // different!
processPayment("SAV2003", 30000) -> Processing with TX: c9d8-... // different!
Fix 2: @Lookup method
Spring overrides an abstract method (or a concrete method you annotate with @Lookup) with a getBean() call. This works but is less explicit than ObjectProvider.
package com.bank.service;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public abstract class PaymentProcessingService {
@Lookup
protected abstract PaymentTransaction createTransaction();
public void processPayment(String accountId, BigDecimal amount) {
PaymentTransaction tx = createTransaction(); // fresh prototype
System.out.println("Processing with TX: " + tx.getTransactionId());
}
}
The downside is that the class must be abstract (or the method overridden), which makes unit testing harder because you cannot create the service with new.
Fix 3: Scoped proxy
package com.bank.service;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class PaymentTransaction {
private final String transactionId = UUID.randomUUID().toString();
public String getTransactionId() {
return transactionId;
}
}
With proxyMode = TARGET_CLASS, Spring injects a CGLIB proxy. Every method call on the proxy delegates to a fresh prototype instance. The singleton never holds the real bean, only the proxy.
How does request scope work?
Request-scoped beans are created once per HTTP request and destroyed after the response is sent. They are useful for holding per-request data like trace IDs and authenticated user information.
package com.bank.context;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;
@Component
@RequestScope
public class RequestContext {
private String traceId;
private String authenticatedUser;
public String getTraceId() { return traceId; }
public void setTraceId(String traceId) { this.traceId = traceId; }
public String getAuthenticatedUser() { return authenticatedUser; }
public void setAuthenticatedUser(String user) { this.authenticatedUser = user; }
}
When a singleton injects a request-scoped bean, Spring automatically creates a scoped proxy. The singleton holds the proxy, and each method call delegates to the RequestContext for the current HTTP request.
package com.bank.service;
import com.bank.context.RequestContext;
import org.springframework.stereotype.Service;
@Service
public class AuditService {
private final RequestContext requestContext; // scoped proxy
public AuditService(RequestContext requestContext) {
this.requestContext = requestContext;
}
public void logAction(String action) {
// Each HTTP request gets its own RequestContext
System.out.println("[" + requestContext.getTraceId() + "] "
+ requestContext.getAuthenticatedUser() + ": " + action);
}
}
// Request 1 (Rajesh): [trace-001] Rajesh: Transfer 10000 to SAV2002
// Request 2 (Priya): [trace-002] Priya: Check balance SAV2003
What are edge cases with bean scopes?
- Prototype beans and @PreDestroy. Spring does not track prototypes after creation.
@PreDestroynever runs. If the prototype holds resources (file handles, connections), you must close them manually. - Session scope in REST APIs. REST APIs are typically stateless with no HTTP session. Session-scoped beans are useless unless you enable sessions explicitly.
- Request scope outside a web request. Accessing a request-scoped bean from a background thread (e.g.,
@Async) throwsIllegalStateException: No thread-bound request found. Pass the data as method parameters instead.
package com.bank.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class NotificationService {
// WRONG: request-scoped bean not available in @Async thread
// private final RequestContext requestContext;
@Async
public void sendNotification(String traceId, String userId, String message) {
// CORRECT: pass request data as parameters
System.out.println("[" + traceId + "] Sending to " + userId + ": " + message);
}
}
What are common mistakes with bean scopes?
WRONG -- assuming prototype beans are destroyed:
package com.bank.service;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype")
public class ReportGenerator {
private java.io.FileWriter writer;
public void generate(String reportPath) throws Exception {
writer = new java.io.FileWriter(reportPath);
writer.write("Account report...");
}
@PreDestroy // NEVER CALLED for prototypes!
public void cleanup() throws Exception {
if (writer != null) writer.close();
}
}
CORRECT -- close resources manually or use try-with-resources:
package com.bank.service;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.io.FileWriter;
@Component
@Scope("prototype")
public class ReportGenerator {
public void generate(String reportPath) throws Exception {
try (FileWriter writer = new FileWriter(reportPath)) {
writer.write("Account report for National Bank of India");
} // auto-closed here
}
}
What should a senior engineer know?
- ObjectProvider vs @Lookup.
ObjectProvideris preferred because it keeps the class concrete and testable.@Lookuprequires the class to be abstract or non-final, making unit testing harder. - Custom scopes. You can create custom scopes by implementing
Scopeand registering it. Examples: tenant scope for multi-tenant SaaS, conversation scope for wizard flows. - Refresh scope (Spring Cloud).
@RefreshScopecreates a proxy that recreates the bean when/actuator/refreshis called, useful for dynamic configuration changes without restart. - Thread scope. There is no built-in thread scope, but
SimpleThreadScopeexists for testing. In production, useThreadLocalor request-scoped beans. - Prototype performance. Creating a new object per call has overhead. For high-throughput paths, consider object pooling (Apache Commons Pool) instead of prototype scope.
What are common interview follow-up questions?
- What happens if you inject a prototype into a singleton? (ONE prototype forever.)
- Does
@PreDestroyrun for prototype beans? (No.) - How does
ObjectProviderwork internally? (It wraps aBeanFactory.getBean()call.) - Can you use request scope outside of a web application? (No, throws
IllegalStateException.) - What is the difference between
@RequestScopeand@Scope("request")? (@RequestScopeis a composed annotation that includesproxyMode = TARGET_CLASSby default.)
Visual Overview
Bean Scope Lifetime:
Singleton: ═══════════════════════════════════════
↑ startup shutdown ↓
Prototype: ──→ new ──→ new ──→ new (no destroy)
Request: ├── req 1 ──┤ ├── req 2 ──┤ ├── req 3 ──┤
Session: ├────────── session 1 ──────────┤
├────────── session 2 ──────────────────┤
Prototype-into-Singleton Trap:
┌─────────────────────────────────┐
│ PaymentProcessingService │
│ (singleton, created ONCE) │
│ │
│ final PaymentTransaction tx │
│ ↑ │
│ Injected ONCE at construction │
│ Same TX for ALL payments! │
└─────────────────────────────────┘
Fix with ObjectProvider:
┌─────────────────────────────────┐
│ PaymentProcessingService │
│ (singleton) │
│ │
│ ObjectProvider<TX> provider │
│ │ │
│ provider.getObject() → new TX │
│ provider.getObject() → new TX │
│ provider.getObject() → new TX │
└─────────────────────────────────┘
Key Takeaway
Spring bean scopes control the lifecycle and sharing of bean instances. Singleton is the default: one instance shared by all injection points and all request threads, with thread safety being your responsibility. Prototype creates a new instance every time the bean is requested, but Spring does not manage destruction, so @PreDestroy never runs. Request scope creates one instance per HTTP request, and session scope creates one per HTTP session. The most critical pitfall is the prototype-into-singleton trap: injecting a shorter-lived scoped bean directly into a singleton gives the singleton one instance at construction time and keeps it forever, completely defeating the scope.
In the National Bank of India's payment gateway, PaymentTransaction is prototype-scoped because each payment must have its own unique transaction ID, amount, and payment method. The PaymentProcessingService is a singleton. Instead of injecting PaymentTransaction directly (which would give one instance forever), Rajesh injects ObjectProvider of PaymentTransaction. Each processPayment() call invokes provider.getObject(), which asks the container for a fresh prototype. Two concurrent payments from Priya and another customer get completely separate transaction objects with different payment IDs. For per-request data like the trace ID and authenticated user, the team uses a RequestContext bean with @RequestScope, which automatically creates a scoped proxy so the singleton AuditService gets the correct context for each HTTP request.
The key rule is: never inject a shorter-lived bean directly into a longer-lived bean. Use ObjectProvider for prototypes and @RequestScope (which includes proxyMode by default) for request-scoped beans.
Remember one sentence: injecting a prototype into a singleton gives you ONE prototype forever; fix it with ObjectProvider.getObject() to get a fresh instance per use.
---