Skip to content
Chapter 08 · Spring Boot

Bean Scopes: Singleton, Prototype, Request and Session ​

Chapter 6 showed the lifecycle of a bean, and chapter 7 how beans are injected into each other. Every bean so far existed exactly once. The scope of a bean decides:

  • how many objects Spring creates for it, and
  • when it creates them (at startup, or later).

The lifecycle from chapter 6 (construct, inject, @PostConstruct, use, @PreDestroy) is the same for every scope; the scope only decides when step 3, construct, happens and how often.

ScopeHow many objectsCreated
singleton (default)one per IoC containereagerly, at startup
prototypea new one every time one is neededlazily, when needed
requestone per HTTP requestlazily, during a request
sessionone per HTTP sessionlazily, during a session
applicationone per servlet context, across containersrarely used

Every experiment below uses the same small classes, which print when they are created and print their hashCode(). Equal hash codes mean the same object; a different hash code means a new object.

Scope 1: singleton (the default) ​

One object per IoC container. Every bean that depends on it receives the same object. In a normal application, with one container, that means one object for the whole application.

There are three ways to write it; all mean the same:

java
@Component                                                  // no @Scope: singleton is the default
public class User { }

@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)             // the constant
public class User { }

@Component
@Scope("singleton")                                         // the plain string
public class User { }

The experiment ​

java
@Component
@Scope("singleton")
public class User {

    public User() {
        System.out.println("User initialization");
    }

    @PostConstruct
    public void init() {
        System.out.println("User object hashCode: " + this.hashCode());
    }
}

Two controllers depend on it:

java
@RestController
@RequestMapping("/api")
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class TestController1 {

    @Autowired
    User user;

    public TestController1() {
        System.out.println("TestController1 instance initialization");
    }

    @PostConstruct
    public void init() {
        System.out.println("TestController1 object hashCode: " + this.hashCode()
                + ", user object hashCode: " + user.hashCode());
    }

    @GetMapping("/fetchUser1")
    public String fetchUser() {
        return "ok";
    }
}

TestController2 is the same, with 2 in the names.

Start the application:

text
User initialization
User object hashCode: 287859212
TestController2 instance initialization
TestController2 object hashCode: 229329107, user object hashCode: 287859212
TestController1 instance initialization
TestController1 object hashCode: 1901700557, user object hashCode: 287859212
  • All three beans were created at startup: singletons are eager.
  • User was created once, and both controllers hold the same User (287859212).

Call GET /api/fetchUser1: nothing is printed. No object is created on a request; everything was built at startup and is reused.

Use it for stateless beans: services, repositories, controllers, clients. Almost every bean in an application is a singleton.

Scope 2: prototype ​

A new object every time one is needed, and lazy: not created at startup unless something created at startup needs it.

java
@Component
@Scope("prototype")                                         // or ConfigurableBeanFactory.SCOPE_PROTOTYPE
public class User { ... }

The experiment ​

  • User: prototype.
  • Student: singleton, depends on User.
  • TestController1: prototype, depends on User and Student.
java
@Component
public class Student {

    @Autowired
    User user;

    public Student() {
        System.out.println("Student instance initialization");
    }

    @PostConstruct
    public void init() {
        System.out.println("Student object hashCode: " + this.hashCode()
                + ", user object hashCode: " + user.hashCode());
    }
}

@RestController
@RequestMapping("/api")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class TestController1 {

    @Autowired
    User user;

    @Autowired
    Student student;

    // constructor and @PostConstruct print as before
    @GetMapping("/fetchUser")
    public String fetchUser() {
        return "ok";
    }
}

Start the application:

text
Student instance initialization
User initialization
User object hashCode: 118363130
Student object hashCode: 1604020967, user object hashCode: 118363130
  • TestController1 is prototype, so it is not created at startup.
  • Student is a singleton, so it is created. It needs a User; User is prototype, but needed now, so one is created for Student.

Call GET /api/fetchUser:

text
TestController1 instance initialization
User initialization
User object hashCode: 1641704889
TestController1 object hashCode: 1787122392, user: 1641704889, student: 1604020967
  • To handle the request, Spring needs a TestController1; it is prototype, so a new one is created.
  • It needs a User: prototype, so a new User (1641704889), not the one Student holds.
  • It needs a Student: singleton, so the existing one (1604020967).

Call it again:

text
TestController1 instance initialization
User initialization
User object hashCode: 1351059270
TestController1 object hashCode: 1581282086, user: 1351059270, student: 1604020967

Another new controller, another new User, the same Student.

A trap: Student is a singleton, so it is created once and its User is injected once. It keeps that same User forever; it does not get a new prototype each time it uses it. To get a fresh prototype on each use from inside a singleton, ask the container each time, for example through an ObjectProvider<User> (provider.getObject()).

Use it for objects that hold state which must not be shared: a builder, a form being filled, a per-task worker.

Scope 3: request ​

One object per HTTP request. Within one request, every bean that needs it gets the same object; the next request gets a new one. It is lazy: it can only exist while a request is being handled.

java
@Component
@Scope("request")
public class User { ... }

The experiment ​

  • TestController1: request scope, depends on User and Student.
  • User: request scope.
  • Student: prototype, depends on User.

Start the application: nothing is printed. All three are lazy, and no request exists yet.

Call GET /api/fetchUser:

text
TestController1 instance initialization
User initialization
User object hashCode: 633462915
Student instance initialization
Student object hashCode: 1501643322, user object hashCode: 633462915
TestController1 object hashCode: 1166294723, user: 633462915, student: 1501643322
  • A request arrived, so a TestController1 is created for it.
  • It needs a User: request scope, and none exists for this request yet, so one is created (633462915).
  • It needs a Student: prototype, so a new one. Student needs a User, and this request already has one: it gets the same User (633462915), not a new one.

Call it again:

text
TestController1 instance initialization
User initialization
User object hashCode: 1351059270
Student instance initialization
Student object hashCode: 1581282086, user object hashCode: 1351059270
TestController1 object hashCode: 1518332982, user: 1351059270, student: 1581282086

A new request: a new controller, a new User (1351059270), shared again by the controller and the Student within this request.

Use it for data that belongs to one request: the current user's id taken from a header, a request id for logging, per-request caches.

The problem: a singleton that needs a request-scoped bean ​

Now make the controller a singleton (the default) while User stays request-scoped:

java
@Component
@Scope("request")
public class User { ... }

@RestController
@RequestMapping("/api")
public class TestController1 {          // singleton: created at startup

    @Autowired
    User user;
    ...
}

Start the application. It fails:

text
TestController1 instance initialization
...
Error creating bean with name 'user': Scope 'request' is not active for the current thread;
consider defining a scoped proxy for this bean if you intend to refer to it from a singleton

Follow the lifecycle: the singleton TestController1 is created at startup and needs a User injected now. But a request-scoped User belongs to an HTTP request, and at startup there is no request. Spring cannot create it, so the controller cannot be completed, and startup fails.

The fix: a scoped proxy ​

The error message says what to do: define a scoped proxy.

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

proxyMode = ScopedProxyMode.TARGET_CLASS tells Spring: when someone needs this bean and no request is active, inject a stand-in. The proxy is a generated subclass of User. At startup it is injected into the controller in place of a real User. Later, every time a method is called on it, the proxy looks up the real User of the current request (creating it if needed) and forwards the call.

Add a print to the handler method (System.out.println("fetchUser called, user = " + user)), then start the application:

text
TestController1 instance initialization
TestController1 object hashCode: 1336762904, user: 943044301

Startup succeeds. No User initialization line: the controller received the proxy, not a real User.

Call GET /api/fetchUser twice:

text
User initialization
User object hashCode: 1761435935
fetchUser called, user = com.example.demo.User@68fd611f

User initialization
User object hashCode: 2001450859
fetchUser called, user = com.example.demo.User@774bb76b

The controller is still the one singleton, holding the one proxy, but each request now gets its own real User, created when the proxy is first used in that request.

Shortcuts. @RequestScope (and @SessionScope below) are request (session) scope with proxyMode = TARGET_CLASS already set:

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

Scope 4: session ​

One object per HTTP session. A session lasts across many requests from the same user (the browser sends its session cookie back each time) until it expires or is invalidated, for example by logging out. It is lazy, like request scope.

The session timeout can be set in application.properties, for example server.servlet.session.timeout=30m.

The experiment ​

  • TestController1: session scope, depends on User.
  • User: singleton.
  • A /logout endpoint ends the session by hand:
java
@RestController
@RequestMapping("/api")
@Scope("session")
public class TestController1 {

    @Autowired
    User user;

    // constructor and @PostConstruct print as before

    @GetMapping("/fetchUser")
    public String fetchUser() {
        return "ok";
    }

    @GetMapping("/logout")
    public String logout(HttpServletRequest request) {
        HttpSession session = request.getSession();
        session.invalidate();                     // end this session
        return "logged out";
    }
}
StepLogWhy
start the applicationUser initialization, User object hashCode: 1010983633User is a singleton: eager. The session-scoped controller is not created.
GET /api/fetchUserTestController1 instance initialization, ... user: 1010983633first request creates a session, and a controller for it
GET /api/fetchUser again(nothing)same session, so the same controller is reused
GET /api/logout(nothing)the session is invalidated
GET /api/fetchUserTestController1 instance initialization, ... user: 1010983633new session, new controller; still the same singleton User

(The second call reuses the session because the client sends the session cookie back. A client that drops cookies starts a new session on every request.)

A singleton that needs a session-scoped bean has the same startup problem as with request scope, with the same fix: proxyMode = ScopedProxyMode.TARGET_CLASS, or @SessionScope.

Use it for per-user state across requests: a shopping cart, a multi-step form, user preferences.

Application scope ​

Application scope means one object per servlet context, shared even by several IoC containers running in the same web application, where a singleton is one per container. Applications almost always run a single container, so in practice it behaves like a singleton and is rarely used.

Summary ​

singletonprototyperequestsession
Objects1 per container1 per injection / lookup1 per HTTP request1 per HTTP session
Createdat startupwhen neededduring a requestduring a session
Into a singletonfineinjected once, then keptneeds a scoped proxyneeds a scoped proxy
Typical useservices, repositories, controllersstateful helpersper-request dataper-user data

Next: Chapter 7 chose between OnlineOrder and OfflineOrder with @Qualifier, so changing the choice meant changing code. Chapter 9 starts the configuration module: it moves that choice into application.properties with @Value.

Check yourself ​

  1. What two things does a bean's scope decide?
  2. What is the default scope? When is such a bean created, and how many exist?
  3. In the prototype experiment, why is a User created at startup although User is prototype?
  4. A singleton Student depends on a prototype User. Does Student get a new User each time it uses it? How could it?
  5. Within one HTTP request, the controller and a prototype Student both need a request-scoped User. How many User objects are created?
  6. A singleton controller injects a bean with @Scope("request"). What happens at startup, and why?
  7. What does proxyMode = ScopedProxyMode.TARGET_CLASS inject, and what happens when a method is called on it?
  8. What is the difference between @RequestScope and @Scope("request")?
  9. A session-scoped controller: when is a new instance created, and when is an existing one reused?
  10. What is the difference between singleton and application scope?
Answers
  1. How many objects are created, and when.
  2. Singleton: created eagerly at startup; one per IoC container.
  3. The singleton Student is created at startup and needs a User, so one is created for it then.
  4. No: it was injected once and kept. Use ObjectProvider<User> (or another lookup in the container) to get a new one on each use.
  5. One: request scope means one per request, shared by everything that needs it during that request.
  6. Startup fails ("Scope 'request' is not active for the current thread"): the singleton is built at startup, when there is no HTTP request to create the request-scoped bean for.
  7. A proxy, a generated subclass that stands in for the bean. Each method call looks up (or creates) the real object for the current request or session and forwards the call.
  8. @RequestScope also sets proxyMode = TARGET_CLASS; plain @Scope("request") has no proxy.
  9. A new one for each new session (the first request, or the first request after the session ends, for example after logout); the same one for every request within a session.
  10. Singleton: one per IoC container. Application: one per servlet context, shared even across containers. With one container, they behave the same.