Skip to content
ExponaBLOG
JAVA, SPRING FRAMEWORK & SPRING BOOT · DAY 025 / 100

Spring bean scopes explained: what developers should know

Singleton is the default — and usually what you want.

Expona Admin5 min read

Introduction

Spring bean scopes explained: what developers should know is one of those fundamentals that separates developers who stumble through production issues from those who build robust systems confidently. Every bean you wire into your application has a scope that determines how many times it gets instantiated and how long it lives, and most of us never think about it until something breaks in production.

The default scope—singleton—is perfect for stateless services, repositories, and controllers. But apply singleton thinking to request-scoped data, and you'll end up with race conditions that only surface under load. In this article, we'll walk through each scope, show you when to use them, and highlight the traps we've seen catch experienced teams.

Singleton: One instance per container

Singleton is the default Spring bean scope, and for good reason. When you declare a bean with no explicit scope annotation, Spring creates exactly one instance for the entire application context and reuses it across every injection point.

java
@Service
public class UserRepository {
    private final DataSource dataSource;
    
    public UserRepository(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    public User findById(Long id) {
        // Query logic here
        return user;
    }
}

Every controller, service, or component that injects UserRepository receives the same instance. This is efficient and usually exactly what you want. Stateless services, repositories, HTTP clients, and utilities are all perfect candidates for singleton scope.

The performance implications are real. Creating database connection pools, loading configuration, or initializing expensive resources happens once at startup rather than repeatedly during request handling. In a high-throughput e-commerce system, we've measured the difference between singleton and prototype services at hundreds of milliseconds per request when scaled to thousands of concurrent users.

The flip side is thread safety. Since all requests share the same instance, any mutable state stored in singleton bean fields becomes a liability. We'll cover that danger in the common mistakes section.

Prototype: New instance every injection

Prototype scope tells Spring to create a fresh instance every single time the bean is requested or injected. Use this when you need isolated state that should not be shared across requests or components.

java
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class OrderProcessor {
    private List<OrderItem> items = new ArrayList<>();
    
    public void addItem(OrderItem item) {
        items.add(item);
    }
    
    public Order process() {
        // Use this.items specific to this processor instance
        return new Order(items);
    }
}

In this example, each time a component injects OrderProcessor, it gets a fresh instance with its own empty items list. This prevents one request's order items from bleeding into another's.

Prototype beans are less common in modern Spring applications than they used to be. We often achieve the same safety by passing data as method parameters instead of storing state in bean fields. However, prototype remains valuable when you're building object factories or when you have complex initialization logic that shouldn't run on every request.

Be aware that Spring doesn't manage the lifecycle of prototype beans after construction. If your prototype bean implements DisposableBean or has cleanup logic, you're responsible for triggering it—Spring won't call destroy methods automatically.

Request scope: Per HTTP request

Request-scoped beans live for the duration of a single HTTP request. Spring creates a new instance when the request arrives and discards it when the response leaves. This is ideal for request-specific data like the authenticated user, request-local caches, or temporary working objects.

java
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private User currentUser;
    private String requestId;
    
    public void setCurrentUser(User user) {
        this.currentUser = user;
    }
    
    public User getCurrentUser() {
        return currentUser;
    }
}

Notice the proxyMode parameter. When you inject a request-scoped bean into a singleton service, Spring wraps it in a proxy so that every method call resolves to the correct instance for the current request. Without the proxy, you'd get an error because the singleton can't hold a reference to a shorter-lived bean.

In a ride-hailing platform, we use request scope for the driver-specific state during matching: the driver's location, available capacity, and preferences. Each request to check ride eligibility gets its own clean context, avoiding cross-request contamination.

Session scope: Per HTTP session

Session-scoped beans persist for the lifetime of an HTTP session. They're useful for user-specific data that should survive across multiple requests—think shopping cart, user preferences, or authentication tokens.

java
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart {
    private List<CartItem> items = new ArrayList<>();
    
    public void addItem(CartItem item) {
        items.add(item);
    }
    
    public List<CartItem> getItems() {
        return new ArrayList<>(items);
    }
}

The same proxy mechanism applies. Inject ShoppingCart into any singleton service, and the proxy ensures you always get the cart for the current user's session.

Session scope works well in traditional servlet-based applications with cookie-based sessions. In modern microservices architectures using stateless token-based auth (JWT), session scope is rarely needed. If you're building a SaaS application where browsers hold long-lived sessions, session scope remains practical.

Common mistakes

Storing request state in singleton fields

This is the classic trap. You inject a singleton service into a controller, store request-specific data in a field, and watch it get overwritten when the next request arrives.

java
@RestController
@RequestMapping("/api/users")
public class UserController {
    @Autowired
    private UserService userService;
    
    private User currentUser; // DANGER: shared across requests
    
    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        currentUser = userService.findById(id); // Race condition
        return ResponseEntity.ok(currentUser);
    }
}

With concurrent requests, currentUser becomes a garbage fire. Request A loads user 1, then request B loads user 2, and suddenly request A sees user 2. Inject a request-scoped bean or pass data as method parameters instead.

Injecting shorter-lived beans into longer-lived ones without proxies

If you try to inject a request-scoped bean into a singleton without the proxy, Spring throws an error at startup. Always include proxyMode when crossing scope boundaries from shorter to longer lifespans.

Prototype beans with expensive initialization

Creating a prototype bean for every injection can become expensive if initialization is heavy. Profile and measure. Often, you're better off keeping the bean singleton and passing request-specific data as method parameters.

Key takeaways

  • Singleton is the default and correct choice for stateless services, repositories, and utilities.
  • Never store mutable request-specific state in singleton bean fields; use request scope or method parameters instead.
  • Request scope is ideal for per-request data like the authenticated user, request IDs, or temporary working objects.
  • Always use proxyMode = ScopedProxyMode.TARGET_CLASS when injecting shorter-lived beans into longer-lived ones.
  • Prototype scope is useful but rare in modern applications; method parameters often provide cleaner isolation.
  • Session scope remains practical in cookie-based web applications but rarely needed in stateless microservices architectures.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles