Spring Bean Scopes Demystified: Singleton vs. Prototype vs. Request

Spring’s Dependency Injection (DI) often feels like magic. You annotate a class with @Component, add an @Autowired field elsewhere, and suddenly everything works. However, treating the IoC (Inversion of Control) container as a black box is dangerous. Misunderstanding how and when your beans are created—specifically their scopes—is a primary cause of nasty production bugs, ranging from subtle race conditions to catastrophic memory leaks.

In the Spring ecosystem, a "Bean Scope" defines more than just visibility; it dictates the lifecycle of a bean instance managed by the container. It controls how long the bean lives, how many instances exist, and when they are discarded.

In this guide, we will dissect the three most critical scopes for backend developers: Singleton, Prototype, and Request. We will compare them not just syntactically, but through the lens of memory usage and thread safety to ensure your applications remain robust under load.

The Default: Singleton Scope

If you do not explicitly define a scope, Spring uses Singleton. This is the bread and butter of Spring Boot applications.

It is vital to distinguish the Spring Singleton from the "Gang of Four" (GoF) Singleton pattern. The GoF pattern ensures one instance of a class exists per ClassLoader. The Spring Singleton, conversely, ensures one instance exists per IoC Container (ApplicationContext). While these often overlap in practice, the distinction matters in complex, multi-module setups.

Memory Implications

Spring defaults to Singleton for one reason: Efficiency. When the container starts, it creates the bean instance once, caches it, and injects that same reference everywhere it is needed. This minimizes garbage collection pressure and initialization overhead, making it ideal for the majority of your beans.

The Critical Warning: Thread Safety

Because a single instance is shared across your entire application, Singleton beans must be stateless.

Consider a scenario where a Singleton service handles incoming web requests. If 100 users hit your endpoint simultaneously, 100 threads will access the exact same instance of that bean. If you store mutable state (like a user's ID or a calculation result) in a class-level field, those threads will overwrite each other's data. This is a classic race condition.

When to Use

Use Singleton scope for 95% of your application components, specifically:

  • Service Layers: Where logic is executed but state is not retained.
  • DAOs / Repositories: Where database connections are managed (usually via a thread-safe connection pool).
  • Utility Classes: Helpers that perform transformations.

The Maverick: Prototype Scope

While Singleton is about sharing, Prototype scope is about isolation. When you define a bean with @Scope("prototype"), the IoC container creates a brand new instance of the bean every single time it is requested—whether that request comes from context.getBean() or via dependency injection into another bean.

Lifecycle Differences

Prototype scope behaves differently regarding lifecycle management. For Singletons, Spring manages the full lifecycle: creation, initialization, and destruction (@PreDestroy).

However, for Prototypes, Spring hands the bean over to the client and then forgets about it. Spring does not manage the complete lifecycle of a prototype bean. The container instantiates, configures, and assembles the prototype, but destruction callbacks are not called. It is the responsibility of the client code to clean up prototype-scoped objects, which can lead to memory leaks if not handled correctly.

Memory Implications

Prototype scope incurs high overhead. Instantiating a complex object graph for every injection point is expensive regarding CPU cycles and memory allocation. Overusing prototypes can lead to significant garbage collection pressure, as short-lived objects churn through the heap.

When to Use

Prototype is rarely needed in standard web development, but it shines in specific scenarios:

  • Stateful Beans (Non-Web): If you need a bean to hold state for a specific background process that isn't tied to an HTTP request.
  • Thread Safety by Isolation: If a class is not thread-safe and cannot be refactored, injecting a new instance for every thread can act as a workaround (though this is often a code smell).

The Web Specialist: Request Scope

In the context of Spring MVC or Spring WebFlux, we gain access to "Web-Aware" scopes. The most prominent of these is Request scope.

Annotating a bean with @RequestScope (or @Scope(value = WebApplicationContext.SCOPE_REQUEST)) tells Spring: "Create a new instance of this bean for a single HTTP request, and destroy it when the request completes."

Use Case: Request Isolation

This is the standard solution for storing user-specific data that needs to be accessible across different layers of your application during a single transaction, without passing parameters through every method signature.

Common examples include:

  • User Context: Storing the authenticated user's ID and roles.
  • Auditing Data: Tracking correlation IDs for logging.
  • Auth Tokens: Holding JWTs for downstream API calls.

Comparison with Prototype

Developers often confuse Prototype and Request scopes. The distinction is subtle but important:

  • Prototype: Creates a new bean per injection point. If Service A and Service B both inject a Prototype bean, they get two different instances.
  • Request: Creates a new bean per HTTP cycle. If Service A and Service B both inject a Request-scoped bean during the same HTTP request, they share the same instance. This allows them to share state (like the current user) for that specific transaction.

The "Gotcha": Injecting Scoped Beans into Singletons

Here lies the most common interview question and production bug regarding scopes: What happens when you inject a Request-scoped bean into a Singleton Service?

The Problem

Remember, a Singleton is created once at application startup. At that moment, Spring attempts to inject its dependencies. If it injects a Request-scoped bean directly, it would inject an instance created at startup. That instance would then be effectively trapped inside the Singleton forever. Every subsequent web request would use that same, stale bean (or fail because no active request existed at startup).

The Solution: Scoped Proxies

To solve this, Spring uses a Scoped Proxy.

When you define a request-scoped bean, you should usually configure it like this:

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserContext {
    // ...
}

(Note: @RequestScope is a composed annotation that applies proxyMode = ScopedProxyMode.TARGET_CLASS automatically.)

Technical Explanation

When proxyMode is active, Spring does not inject the actual UserContext instance into your Singleton service. Instead, it injects a CGLIB Proxy (a subclass generated at runtime).

When your Singleton service calls userContext.getUserId(), it is actually calling the proxy. The proxy then performs a lookup to fetch the real UserContext instance bound to the current thread's HTTP request and delegates the method call to it. This ensures thread safety and correct scoping, even inside a Singleton.

Alternative

If you prefer to avoid proxies, you can inject an ObjectFactory or Provider and call .getObject() whenever you need the instance. This performs a lazy lookup, though proxies are generally cleaner to read.

Summary: Choosing the Right Scope

Understanding scopes is about balancing memory efficiency with thread safety. Here is your mental model for decision making:

ScopeLifecycleThread Safety StrategyIdeal Use Case
SingletonOne per ContainerMust be statelessServices, DAOs, Utilities
PrototypeOne per InjectionSafe via isolation (unique instances)Stateful non-web beans, heavy calculation objects
RequestOne per HTTP RequestSafe via isolation (per request)User context, Correlation IDs, Auth tokens

Final Advice

  1. Stick to Singleton for 95% of your beans. It is the most memory-efficient.
  2. Use Request Scope when you need to carry state throughout an HTTP transaction.
  3. Use Prototype sparingly, as it incurs performance penalties and requires manual lifecycle management if resources need cleanup.

Take a moment to audit your current codebase. Look for Singleton beans (the default) that hold mutable fields. If you find them, you likely have a hidden concurrency bug waiting to happen. Refactor them to be stateless or move that state into a Request-scoped bean.