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, with ObjectProvider 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 called getBean(), 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 per ServletContext (web applications only).
  • websocket: one instance per WebSocket session (WebSocket applications only).
You set the scope with @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);
    }
  }
}
 OUTPUT of the above code
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.
Singletons and thread safety: in a web application, one singleton bean serves every request, on many threads at the same time. Singleton beans should therefore be stateless - holding only their injected dependencies and configuration, never per-request or per-user data in fields. Data about the current request belongs in method parameters and local variables, or in a request-scoped bean.

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));
    }
  }
}
 OUTPUT of the above code
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 new ShoppingCart, 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 @PreDestroy when 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");
    }
  }
}
 OUTPUT of the above code
Tom Jones uses PriceCalculation #1
Nancy Smith uses PriceCalculation #1
Ethan Hardy uses PriceCalculation #1
All three customers shared calculation #1. If 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 an ObjectProvider<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());
    }
  }
}
 OUTPUT of the above code
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.
  • ObjectProvider is 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 @Lookup method, Spring generated a subclass of LookupCheckout (its name contains $$SpringCGLIB$$) that overrides newCalculation(). The method body we wrote is never called. Because of the subclass, the class and the method must not be final or private, and @Lookup works only for beans found by component scanning, not for objects returned from @Bean methods.

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 (per ServletContext). 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
  }
}
This looks like the prototype trap again - a short-lived bean injected into a singleton - but it works, because the web scope annotations set 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();
    }
  }
}
 OUTPUT of the above code
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 the request and session scopes in the same way.
  • AuditService holds a UserContext$$SpringCGLIB$$0 - the scoped proxy - not a real UserContext.
  • Thread request-1 set the user to Tom Jones on its own instance. Thread request-2 got a fresh instance, so its first log line shows the default nobody, not Tom - the two threads never saw each other's data, although they shared one singleton AuditService.
  • 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 ObjectProvider wherever a singleton needs a fresh one, and remember that the container will not destroy them. If the object needs no dependencies, plain new is 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 with ObjectProvider 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.