Spring Bean Scopes: Singleton, Prototype, Request, Session
Introduction
This tutorial explains bean scopes in Spring - the setting that decides how many instances of a bean the container creates and how long each one lives. We will start with what a scope is, then look at the default singleton scope, including how it differs from the Singleton design pattern and what it means for thread safety. We will then use the prototype scope and run into its classic trap: a prototype bean injected into a singleton. Next we will fix that trap in two ways, withObjectProvider and with @Lookup methods. Lastly, we will cover the web scopes - request, session and application - and the scoped proxies that make them work, see a scoped proxy in action with Spring's thread scope, and finish with a guide to choosing a scope.(This tutorial builds on Spring dependency injectionClick to read tutorial: Spring Dependency Injection: Constructor vs Setter vs Field; you should know how beans are injected into each other.)
What is a bean scope?
When the container needs a bean - because you calledgetBean(), or because another bean depends on it - the bean's scope decides whether it returns an existing instance or creates a new one. Spring has these scopes -
singleton(the default): one instance per container, shared by everyone.prototype: a new instance every time the bean is requested.request: one instance per HTTP request (web applications only).session: one instance per HTTP session (web applications only).application: one instance perServletContext(web applications only).websocket: one instance per WebSocket session (WebSocket applications only).
@Scope on a @Component class or a @Bean method. The web scopes also have shortcut annotations: @RequestScope, @SessionScope and @ApplicationScope.
Singleton scope
Every bean in the earlier tutorials was a singleton: the container creates it once, at startup, and every lookup and every injection gets that same object. The example checks this, and then starts a second container from the same configuration -Singleton beans: one instance per container
//SingletonScopeApp.java
package com.javabrahman.spring.scopes.singleton;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
@Component
class ExchangeRates {
private static int instances = 0;
final int instanceNumber = ++instances;
}
@Component
class InvoiceService {
final ExchangeRates rates;
InvoiceService(ExchangeRates rates) {
this.rates = rates;
}
}
@Configuration
@ComponentScan
class AppConfig {
}
public class SingletonScopeApp {
public static void main(String[] args) {
try (var first = new AnnotationConfigApplicationContext(AppConfig.class);
var second = new AnnotationConfigApplicationContext(AppConfig.class)) {
ExchangeRates a = first.getBean(ExchangeRates.class);
ExchangeRates b = first.getBean(ExchangeRates.class);
System.out.println("Two lookups, same object: " + (a == b) + " (instance #" + a.instanceNumber + ")");
System.out.println("Injected into InvoiceService, same object: " + (first.getBean(InvoiceService.class).rates == a));
System.out.println("Second container's instance: #" + second.getBean(ExchangeRates.class).instanceNumber);
}
}
}
Two lookups, same object: true (instance #1) Injected into InvoiceService, same object: true Second container's instance: #2
Explanation of the code
- Within one container, both lookups and the injected field are the same object, instance #1.
- The second container created its own instance, #2. A Spring singleton is one instance per container, not one per JVM.
- That is the difference from the Singleton design patternClick to read: Singleton design pattern, where the class itself prevents a second instance with a private constructor and a static field. A Spring singleton is an ordinary class with a public constructor; the container decides to create only one. Tests can still create as many as they like with
new.
Prototype scope
A prototype bean is created anew every time it is requested. Use it for objects that hold state for one use - a builder, a calculation in progress, a shopping cart for one checkout -Prototype beans: a new instance for every request to the container
//PrototypeScopeApp.java
package com.javabrahman.spring.scopes.prototype;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // the same as @Scope("prototype")
class ShoppingCart {
static int instances = 0;
final int cartNumber = ++instances;
final List<String> items = new ArrayList<>();
}
@Configuration
@ComponentScan
class AppConfig {
}
public class PrototypeScopeApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println("Context started, carts created so far: " + ShoppingCart.instances);
ShoppingCart tomsCart = context.getBean(ShoppingCart.class);
tomsCart.items.add("Java 8 in Action");
ShoppingCart nancysCart = context.getBean(ShoppingCart.class);
nancysCart.items.add("Effective Java");
System.out.println("Cart #" + tomsCart.cartNumber + ": " + tomsCart.items);
System.out.println("Cart #" + nancysCart.cartNumber + ": " + nancysCart.items);
System.out.println("Same object: " + (tomsCart == nancysCart));
}
}
}
Context started, carts created so far: 0 Cart #1: [Java 8 in Action] Cart #2: [Effective Java] Same object: false
Explanation of the code
- Each
getBean()call created a newShoppingCart, so the two customers' items stayed separate. - Prototype beans are not created at startup; the container creates one only when asked.
- After handing a prototype out, the container forgets it: it does not keep a reference and does not call its destroy callbacks such as
@PreDestroywhen the context closes. Releasing any resources a prototype holds is the caller's job. The tutorial on the bean lifecycleClick to read tutorial: Spring Bean Lifecycle: @PostConstruct, @PreDestroy and More shows this.
The trap: a prototype bean inside a singleton
Prototype scope means "new instance per request to the container", and a singleton asks the container only once - when it is created. So a prototype injected into a singleton's constructor is created once and then shared, exactly like a singleton -A prototype bean injected into a singleton is created only once
//PrototypeInSingletonApp.java
package com.javabrahman.spring.scopes.trap;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@Component
@Scope("prototype")
class PriceCalculation {
private static int instances = 0;
final int number = ++instances;
}
@Service
class CheckoutService {
private final PriceCalculation calculation; // injected once, when CheckoutService is created
CheckoutService(PriceCalculation calculation) {
this.calculation = calculation;
}
void checkout(String customer) {
System.out.println(customer + " uses PriceCalculation #" + calculation.number);
}
}
@Configuration
@ComponentScan
class AppConfig {
}
public class PrototypeInSingletonApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
CheckoutService checkout = context.getBean(CheckoutService.class);
checkout.checkout("Tom Jones");
checkout.checkout("Nancy Smith");
checkout.checkout("Ethan Hardy");
}
}
}
Tom Jones uses PriceCalculation #1 Nancy Smith uses PriceCalculation #1 Ethan Hardy uses PriceCalculation #1
PriceCalculation kept state in fields, the customers would see each other's data - and in a web application, on concurrent threads, they would overwrite it. The singleton needs a way to ask the container for a fresh prototype each time it needs one.
Fix 1 and fix 2: ObjectProvider and @Lookup
The cleanest fix is to inject anObjectProvider<T> instead of the bean. Each call to its getObject() asks the container again, so a prototype bean is created each time. The second fix is a @Lookup method: an abstract or placeholder method that Spring overrides at runtime to return a bean from the container -
Getting a fresh prototype from a singleton with ObjectProvider and with @Lookup
//PrototypeFixesApp.java
package com.javabrahman.spring.scopes.fixes;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@Component
@Scope("prototype")
class PriceCalculation {
private static int instances = 0;
final int number = ++instances;
}
@Service
class ProviderCheckout {
private final ObjectProvider<PriceCalculation> calculations;
ProviderCheckout(ObjectProvider<PriceCalculation> calculations) {
this.calculations = calculations;
}
void checkout(String customer) {
PriceCalculation calculation = calculations.getObject(); // a new prototype each call
System.out.println("ObjectProvider: " + customer + " uses PriceCalculation #" + calculation.number);
}
}
@Service
class LookupCheckout {
void checkout(String customer) {
System.out.println("@Lookup: " + customer + " uses PriceCalculation #" + newCalculation().number);
}
@Lookup // Spring overrides this method to return getBean(PriceCalculation.class)
PriceCalculation newCalculation() {
return null;
}
}
@Configuration
@ComponentScan
class AppConfig {
}
public class PrototypeFixesApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
ProviderCheckout providerCheckout = context.getBean(ProviderCheckout.class);
providerCheckout.checkout("Tom Jones");
providerCheckout.checkout("Nancy Smith");
LookupCheckout lookupCheckout = context.getBean(LookupCheckout.class);
lookupCheckout.checkout("Ethan Hardy");
lookupCheckout.checkout("Harry Major");
System.out.println("LookupCheckout runs as " + lookupCheckout.getClass().getSimpleName());
}
}
}
ObjectProvider: Tom Jones uses PriceCalculation #1 ObjectProvider: Nancy Smith uses PriceCalculation #2 @Lookup: Ethan Hardy uses PriceCalculation #3 @Lookup: Harry Major uses PriceCalculation #4 LookupCheckout runs as LookupCheckout$$SpringCGLIB$$0
Explanation of the code
- Every checkout got a new
PriceCalculation, numbered #1 to #4, in both singletons. ObjectProvideris plain dependency injection - it is easy to read and easy to replace in a test with a provider that returns a test object. Prefer it.- For the
@Lookupmethod, Spring generated a subclass ofLookupCheckout(its name contains$$SpringCGLIB$$) that overridesnewCalculation(). The method body we wrote is never called. Because of the subclass, the class and the method must not befinalorprivate, and@Lookupworks only for beans found by component scanning, not for objects returned from@Beanmethods.
Web scopes: request, session and application
The web scopes exist only in a web-aware application context, such as the one Spring Boot creates for a Spring MVC application -@RequestScope: one instance per HTTP request. Use it for data gathered while handling one request, such as the current user or a correlation id.@SessionScope: one instance per HTTP session, kept across a user's requests until the session expires. Use it for per-user state such as a wizard in progress. Keep it small and serializable, since sessions may be stored or replicated.@ApplicationScope: one instance per web application (perServletContext). It is close to a singleton, but it is also visible as a servlet context attribute.
A request-scoped bean used from a singleton controller
@Component
@RequestScope // one RequestInfo per HTTP request
class RequestInfo {
private String userName;
// getter and setter
}
@RestController
class GreetingController { // a singleton serving every request
private final RequestInfo requestInfo; // in fact a proxy
GreetingController(RequestInfo requestInfo) {
this.requestInfo = requestInfo;
}
@GetMapping("/greeting")
String greet() {
return "Hello, " + requestInfo.getUserName(); // goes to the current request's RequestInfo
}
}
proxyMode = ScopedProxyMode.TARGET_CLASS. Spring injects a scoped proxy: a stand-in object of the same type that, on every method call, looks up the right instance for the current request or session and passes the call on to it. (The Proxy design patternClick to read: Proxy design pattern describes the idea.) Web controllers and REST APIs are covered in module 3 of this series.
A scoped proxy in action: the thread scope
A request scope needs a running web server, but Spring ships a non-web scope that works the same way:SimpleThreadScope, which keeps one instance per thread. It is not registered by default, so the example registers it, declares a thread-scoped bean with a scoped proxy, injects it into a singleton, and calls the singleton from two threads -
A thread-scoped bean behind a scoped proxy, injected into a singleton
//ScopedProxyApp.java
package com.javabrahman.spring.scopes.proxy;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.context.support.SimpleThreadScope;
import org.springframework.stereotype.Component;
@Component
@Scope(value = "thread", proxyMode = ScopedProxyMode.TARGET_CLASS)
class UserContext {
private String user = "nobody";
String getUser() {
return user;
}
void setUser(String user) {
this.user = user;
}
}
@Component
class AuditService { // a singleton, shared by both threads
private final UserContext userContext;
AuditService(UserContext userContext) {
this.userContext = userContext;
}
void log(String action) {
System.out.println(Thread.currentThread().getName() + ": " + action + " by " + userContext.getUser());
}
String injectedClass() {
return userContext.getClass().getSimpleName();
}
}
@Configuration
@Import({UserContext.class, AuditService.class})
class AppConfig {
}
public class ScopedProxyApp {
public static void main(String[] args) throws InterruptedException {
try (var context = new AnnotationConfigApplicationContext()) {
context.getBeanFactory().registerScope("thread", new SimpleThreadScope());
context.register(AppConfig.class);
context.refresh();
AuditService audit = context.getBean(AuditService.class);
UserContext userContext = context.getBean(UserContext.class);
System.out.println("AuditService holds a " + audit.injectedClass());
Thread tom = Thread.ofPlatform().name("request-1").start(() -> {
userContext.setUser("Tom Jones");
audit.log("viewed invoice 17");
});
tom.join();
Thread nancy = Thread.ofPlatform().name("request-2").start(() -> {
audit.log("viewed invoice 18"); // this thread never set a user
userContext.setUser("Nancy Smith");
audit.log("paid invoice 18");
});
nancy.join();
}
}
}
AuditService holds a UserContext$$SpringCGLIB$$0 request-1: viewed invoice 17 by Tom Jones request-2: viewed invoice 18 by nobody request-2: paid invoice 18 by Nancy Smith
Explanation of the code
registerScope("thread", new SimpleThreadScope())adds the custom scope before the configuration is registered and the context is refreshed. Web application contexts register therequestandsessionscopes in the same way.AuditServiceholds aUserContext$$SpringCGLIB$$0- the scoped proxy - not a realUserContext.- Thread
request-1set the user to Tom Jones on its own instance. Threadrequest-2got a fresh instance, so its first log line shows the defaultnobody, not Tom - the two threads never saw each other's data, although they shared one singletonAuditService. - Replace "thread" with "request" and this is exactly how a request-scoped bean behaves in a web application.
Thread.ofPlatform()is the Java 21 thread builder; the example names the threads so that the output shows which one is running.
Choosing a scope
- Singleton - for almost everything: services, repositories, controllers, clients, configuration. Keep these beans stateless.
- Prototype - for stateful objects used once, when you want the container to build them (with their own dependencies injected). Inject an
ObjectProviderwherever a singleton needs a fresh one, and remember that the container will not destroy them. If the object needs no dependencies, plainnewis often simpler. - Request - for data belonging to the current HTTP request.
- Session - for small per-user state that must survive between requests. In REST APIs, which are usually stateless, it is rarely needed.
- Application - rarely needed; a singleton usually does the job.
Summary
In this tutorial we first saw what a bean scope is and which scopes Spring has. We confirmed that a singleton is one instance per container, unlike the Singleton design pattern, and that singletons should be stateless. We created prototype beans, then saw the trap of injecting a prototype into a singleton, and fixed it withObjectProvider and with a @Lookup method. Next we looked at the request, session and application web scopes and the scoped proxies behind them, and watched a scoped proxy route calls to per-thread instances with Spring's thread scope. Lastly, we summed up when to use each scope. The next tutorial follows a bean from creation to destruction: the Spring bean lifecycleClick to read tutorial: Spring Bean Lifecycle: @PostConstruct, @PreDestroy and More.Spring Tutorials
Spring Dependency Injection: Constructor vs Setter vs FieldClick to read tutorial: Spring Dependency Injection: Constructor vs Setter vs Field Spring Bean Lifecycle: @PostConstruct, @PreDestroy and MoreClick to read tutorial: Spring Bean Lifecycle: @PostConstruct, @PreDestroy and More What is Spring? Spring Framework vs Spring Boot ExplainedClick to read tutorial: What is Spring? Spring Framework vs Spring Boot Explained Spring IoC Container and ApplicationContext ExplainedClick to read tutorial: Spring IoC Container and ApplicationContext Explained Spring Beans: @Component, @Bean and @ConfigurationClick to read tutorial: Spring Beans: @Component, @Bean and @Configuration Spring @Qualifier, @Primary and Conditional BeansClick to read tutorial: Spring @Qualifier, @Primary and Conditional Beans Spring Expression Language (SpEL) and @Value ExplainedClick to read tutorial: Spring Expression Language (SpEL) and @Value Explained
Spring Dependency Injection: Constructor vs Setter vs FieldClick to read tutorial: Spring Dependency Injection: Constructor vs Setter vs Field Spring Bean Lifecycle: @PostConstruct, @PreDestroy and MoreClick to read tutorial: Spring Bean Lifecycle: @PostConstruct, @PreDestroy and More What is Spring? Spring Framework vs Spring Boot ExplainedClick to read tutorial: What is Spring? Spring Framework vs Spring Boot Explained Spring IoC Container and ApplicationContext ExplainedClick to read tutorial: Spring IoC Container and ApplicationContext Explained Spring Beans: @Component, @Bean and @ConfigurationClick to read tutorial: Spring Beans: @Component, @Bean and @Configuration Spring @Qualifier, @Primary and Conditional BeansClick to read tutorial: Spring @Qualifier, @Primary and Conditional Beans Spring Expression Language (SpEL) and @Value ExplainedClick to read tutorial: Spring Expression Language (SpEL) and @Value Explained