Managing Concurrency with Spring Bean Scopes: A Complete Guide
Singleton Beans, Multiple Threads, Zero Data Leaks: The Counterintuitive Magic Behind Spring's Thread Safety
Table of contents
- The Singleton Paradox: How Spring Handles Concurrent Requests
- When Singletons Go Wrong: The Perils of Mutable State
- Understanding Spring Bean Scopes
- Configuring Bean Scopes in Spring
- Real-World Example: Building a Spring Backend for React
- Integration with React Frontend
- Performance Implications of Bean Scopes
- Testing for Scope-Related Issues
- Best Practices for Bean Scope Management
- Conclusion
"Why is user data leaking between accounts in our production app?" The developer's message hit our Slack channel at 2 AM. The culprit? A seemingly innocent Spring bean storing user information in an instance variable.
You might be wondering: if Spring beans are singletons by default, how do they handle thousands of concurrent requests without catastrophic data contamination? It's a paradox that confuses even experienced developers—and getting it wrong can lead to security breaches, data corruption, and those dreaded middle-of-the-night emergencies.
This guide will demystify Spring's bean scoping system, revealing how the framework elegantly manages concurrent requests despite its singleton-first approach. We'll explore when to break away from the default scope, how to match bean lifecycles to your state management needs, and how to test for the subtle bugs that scope-related issues can create.
The Singleton Paradox: How Spring Handles Concurrent Requests
If you're new to Spring, you might wonder: if beans are singletons by default, how can the framework possibly handle multiple concurrent requests without users seeing each other's data?
The answer lies in stateless design. Spring's architecture encourages developers to create stateless services, where beans don't store request-specific data as instance variables. Instead, data flows through method parameters and return values.
Consider this example of a well-designed stateless service:
@Service
public class TaxCalculatorService {
public double calculateTax(double income, String state) {
double taxRate = getTaxRateForState(state);
return income * taxRate;
}
private double getTaxRateForState(String state) {
return state.equals("IL") ? 0.0495 : 0.05;
}
}
This service can safely handle concurrent requests because each thread maintains its own execution stack with local variables that don't interfere with other threads.
When Singletons Go Wrong: The Perils of Mutable State
To understand why stateless design matters, consider this problematic implementation:
@Service
public class ShoppingCartService {
private List<String> items = new ArrayList<>();
private String username;
public void setUsername(String username) {
this.username = username;
}
public void addItem(String item) {
items.add(item);
}
public List<String> getItems() {
return items;
}
public void checkout() {
System.out.println("Processing order for: " + username);
System.out.println("Items: " + items);
items.clear();
}
}
This service stores user-specific data as instance variables. Since it's a singleton, all users share the same instance, leading to these problems:
User A could see User B's items or username
One user's checkout could clear another user's cart
Race conditions could corrupt data as concurrent requests modify the same lists
Understanding Spring Bean Scopes
Spring offers several scopes to address different state management needs:
Singleton Scope (Default)
One instance per Spring container, shared across all requests.
Best for: Stateless services, immutable data, thread-safe utilities
Example use case: Tax calculators, email formatters, validation services
Request Scope
Creates a new instance for each HTTP request.
Best for: Request-specific context, tracking request metadata
Example use case: Request loggers, audit contexts, request validators
Session Scope
Creates a new instance for each user session.
Best for: User-specific data that persists across multiple requests
Example use case: Shopping carts, user preferences, multi-step wizards
Prototype Scope
Creates a new instance each time requested.
Best for: Stateful components that must be unique
Example use case: Complex object builders, non-shared resources
Configuring Bean Scopes in Spring
Configuring a bean's scope is straightforward:
@Component
@RequestScope // or @SessionScope, or @Scope("prototype")
public class UserContext {
private String userId;
private Instant loginTime;
// Getters and setters
}
Real-World Example: Building a Spring Backend for React
When developing a Spring backend for a React frontend, managing state becomes especially important. Let's look at a practical example that uses different scopes appropriately:
// Singleton - Stateless service
@Service
public class ProductService {
private final ProductRepository repository;
public ProductService(ProductRepository repository) {
this.repository = repository;
}
public List<Product> findProducts(String category) {
return repository.findByCategory(category);
}
}
// Request-scoped - Tracks request-specific data
@Component
@RequestScope
public class RequestAuditContext {
private final String requestId = UUID.randomUUID().toString();
private Instant startTime = Instant.now();
public void logRequest(String endpoint) {
long duration = Duration.between(startTime, Instant.now()).toMillis();
System.out.println("Request " + requestId + " to " + endpoint + " took " + duration + "ms");
}
}
// Session-scoped - Maintains user state across requests
@Component
@SessionScope
public class ShoppingCart {
private final List<CartItem> items = new ArrayList<>();
public void addItem(CartItem item) {
items.add(item);
}
public List<CartItem> getItems() {
return Collections.unmodifiableList(items);
}
}
Integration with React Frontend
Spring's session management works well with React frontends, but requires some configuration:
Cookie-Based Sessions
For traditional cookie-based sessions:
@Configuration
public class SessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setSameSite("Lax");
serializer.setCookiePath("/");
serializer.setUseSecureCookie(true);
return serializer;
}
}
React needs to include credentials with requests:
fetch('https://api.example.com/api/user/profile', {
credentials: 'include' // Important for sending cookies
})
JWT-Based Authentication
For token-based approaches:
@RestController
public class AuthController {
@Autowired
private JwtTokenProvider tokenProvider;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
// Authenticate user
String token = tokenProvider.generateToken(username);
return ResponseEntity.ok(new JwtResponse(token));
}
}
React would store and send the token:
fetch('https://api.example.com/user/cart', {
headers: {
'Authorization': `Bearer ${token}`
}
})
Performance Implications of Bean Scopes
Bean scope selection impacts application performance in several ways:
Scope | Memory Usage | Creation Cost | Relative Performance |
Singleton | Lowest | Once at startup | Fastest (100%) |
Request | Medium | Once per request | Medium (70-90%) |
Session | Potentially high | Once per session | Medium-Fast (80-95%) |
Prototype | Varies | On each retrieval | Slowest (50-80%) |
For most applications, these performance differences are negligible compared to database access and external API calls. Still, high-throughput applications should minimize the use of request-scoped beans on critical paths.
Testing for Scope-Related Issues
Detecting scope-related problems requires specific testing approaches:
Concurrent Request Testing
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ConcurrentRequestTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void testConcurrentRequests() throws InterruptedException {
int threadCount = 10;
CountDownLatch latch = new CountDownLatch(threadCount);
AtomicInteger successCount = new AtomicInteger(0);
for (int i = 0; i < threadCount; i++) {
final String userId = "user" + i;
new Thread(() -> {
try {
ResponseEntity<String> response = restTemplate.postForEntity(
"/api/profile",
Map.of("userId", userId, "action", "update"),
String.class
);
if (response.getBody().contains(userId)) {
successCount.incrementAndGet();
}
} finally {
latch.countDown();
}
}).start();
}
latch.await(10, TimeUnit.SECONDS);
assertEquals(threadCount, successCount.get());
}
}
Session Contamination Testing
Create tests that verify user A never sees user B's data:
@Test
public void testNoSessionContamination() {
// Create two separate sessions
TestRestTemplate userA = new TestRestTemplate();
TestRestTemplate userB = new TestRestTemplate();
// Add different items to each cart
userA.postForEntity("/api/cart/add", new CartItem("item-A"), Void.class);
userB.postForEntity("/api/cart/add", new CartItem("item-B"), Void.class);
// Verify each user sees only their own items
ResponseEntity<Cart> userACart = userA.getForEntity("/api/cart", Cart.class);
ResponseEntity<Cart> userBCart = userB.getForEntity("/api/cart", Cart.class);
assertTrue(userACart.getBody().containsItem("item-A"));
assertFalse(userACart.getBody().containsItem("item-B"));
assertTrue(userBCart.getBody().containsItem("item-B"));
assertFalse(userBCart.getBody().containsItem("item-A"));
}
Best Practices for Bean Scope Management
Default to Stateless Singletons: Design services to be stateless when possible
Match Scope to State Requirements:
Application-wide state → Singleton with thread-safe collections
User-specific state → Session scope
Request-specific state → Request scope
Prefer Method Parameters: Pass data through method parameters rather than storing in instance variables
Use ThreadLocal Carefully: For framework code that needs request context without dependency injection
Document Scope Decisions: Add comments or documentation explaining scope choices for non-standard cases
Conclusion
Understanding Spring bean scopes is crucial for building robust, scalable applications that handle concurrent requests correctly. By matching the right scope to your state management needs, you can create applications that are both thread-safe and efficient.
The next time you create a Spring bean, ask yourself: "What is the right scope for this component's state?" By making this a conscious decision rather than accepting the default, you'll build more reliable applications and avoid the subtle bugs that come from improper state management.
Remember, in most cases, following Spring's push toward stateless design with singleton beans is the right approach. Only reach for other scopes when you have a specific state management requirement that demands it.