Back to IoC Container & Bean Lifecycle
Q4Question 4 of 4

What are the Bean Scopes?

Key Concepts
  • Singleton (default): one instance per ApplicationContext, shared by all injection points and all threads; thread safety is your responsibility.
  • Prototype: a new instance every time the bean is requested via getBean() or injection; Spring does not manage destruction, so @PreDestroy never runs.
  • Request scope: one instance per HTTP request, destroyed after the response is sent.
  • Session scope: one instance per HTTP session, destroyed when the session expires.
  • The prototype-into-singleton trap: injecting a prototype into a singleton gives the singleton ONE prototype instance forever, completely defeating the scope.
  • Fix with ObjectProvider<T>: inject a factory and call getObject() per use to get a fresh prototype each time.
  • Fix with scoped proxy (proxyMode = TARGET_CLASS): a CGLIB proxy delegates to the correct scope's bean on every method call.

Definition

Singleton means one instance for all; prototype means a new instance every time; injecting a prototype into a singleton gives you ONE prototype forever; fix with ObjectProvider.getObject() to get a fresh instance per use.
  • 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 @PreDestroy never 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 |

JAVA
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;
    }
}
JAVA
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
    }
}
TEXT
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 |

JAVA
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:

JAVA
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!
    }
}
TEXT
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.

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

JAVA
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

JAVA
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.

JAVA
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.

JAVA
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);
    }
}
TEXT
// 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. @PreDestroy never 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) throws IllegalStateException: No thread-bound request found. Pass the data as method parameters instead.
JAVA
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:

JAVA
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:

JAVA
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. ObjectProvider is preferred because it keeps the class concrete and testable. @Lookup requires the class to be abstract or non-final, making unit testing harder.
  • Custom scopes. You can create custom scopes by implementing Scope and registering it. Examples: tenant scope for multi-tenant SaaS, conversation scope for wizard flows.
  • Refresh scope (Spring Cloud). @RefreshScope creates a proxy that recreates the bean when /actuator/refresh is called, useful for dynamic configuration changes without restart.
  • Thread scope. There is no built-in thread scope, but SimpleThreadScope exists for testing. In production, use ThreadLocal or 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 @PreDestroy run for prototype beans? (No.)
  • How does ObjectProvider work internally? (It wraps a BeanFactory.getBean() call.)
  • Can you use request scope outside of a web application? (No, throws IllegalStateException.)
  • What is the difference between @RequestScope and @Scope("request")? (@RequestScope is a composed annotation that includes proxyMode = TARGET_CLASS by default.)

Visual Overview

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate