Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Learn › Spring › IoC container and DI

Bean scopes

It is a prototype bean, so why is there only one object?

A scope determines how many objects of a bean the container creates and how long they are used. Singleton means one per container, prototype means a new one every time the container is asked for the bean, and request means one per HTTP request. But when a singleton bean has a bean of another scope injected into it, something unexpected happens.

Checked against real execution · A Spring Boot 4.1.1 server was started, and HTTP requests were sent for all 9 combinations and checked against the results

Concepts in this lab

Bean scope
This is the range over which a bean instance is created and kept. The default, singleton, creates only one instance per container, and everyone shares it. prototype creates a new instance each time it is requested. In a web environment there are also the request scope, which creates one per HTTP request, and the session scope, which creates one per session.
Spring bean
This is an object that the Spring IoC container creates and manages. Unlike an object that a developer creates directly with new, the container injects its dependencies and manages its initialization and destruction, and additional features such as transactions and AOP can be applied to it through a proxy.
ObjectProvider
This is a Spring tool that lets you retrieve a bean object from the container at the moment you need it instead of having it injected right away. If you declare the type of a constructor parameter or a field as, for example, ObjectProvider<Cart>, the container injects it, and each call to getObject() asks the container for a bean of that type. So for a prototype bean each call returns a new object, and for a request-scoped bean it returns the object of the request that is being handled now. It is mainly used when a singleton bean uses a bean that is shorter-lived than itself. If you need to keep using the same object within one unit of work, retrieve it only once and keep it in a local variable.
Scoped proxy
This is a stand-in object that is injected in place of the real object when a bean of a scope other than singleton is injected into another bean. If you specify proxyMode = ScopedProxyMode.TARGET_CLASS in @Scope, the container creates a proxy that extends that class and injects it. Each time one of its methods is called, the proxy looks up the real object for that scope in the container and passes the call on to it. Used with the request scope, it lets a singleton bean use a different object for each request without any change to its code, and @RequestScope has this setting built in. Used with the prototype scope, it creates a new object every time a method is called, so take care.
Proxy
This is a stand-in object that receives calls in place of the real object. When it receives a call, it does some additional work and then passes the call on to the real object. Spring wraps a bean that needs a feature such as AOP or @Transactional in such an object and registers it in the container, so what other beans receive by injection is this stand-in, not the real object.

Try these

Running with the default settings

  1. Container startup: The three beans orderController, orderService, and cart are registered. orderController and orderService are singletons, and the scope of cart is prototype.
  2. Creating the orderService bean: The constructor requires a Cart, so the container is asked for a Cart. A prototype bean is created anew each time it is asked for, so the container creates Cart #1 and passes it. OrderService stores this object in a field.
  3. Startup is finished. Cart objects created so far: 1.
  4. Request 1: GET /order comes in. The order() method of OrderController calls orderService.cartNo() twice.
  5. cartNo() call #1: It calls no() on Cart #1, the object held in the field. Injection happened only once, when OrderService was created, so no new object is created even though the bean is a prototype.
  6. cartNo() call #2: It calls no() on Cart #1, the object held in the field. Injection happened only once, when OrderService was created, so no new object is created even though the bean is a prototype.
  7. The response to request 1 is "1,1".
  8. Request 2: GET /order comes in. The order() method of OrderController calls orderService.cartNo() twice.
  9. cartNo() call #1: It calls no() on Cart #1, the object held in the field. Injection happened only once, when OrderService was created, so no new object is created even though the bean is a prototype.
  10. cartNo() call #2: It calls no() on Cart #1, the object held in the field. Injection happened only once, when OrderService was created, so no new object is created even though the bean is a prototype.
  11. The response to request 2 is "1,1".

Result of this run

Code

@Component
@Scope("prototype")
class Cart {
    private static int created = 0;   // number of Cart objects created so far
    private final int no;             // which object this is, in order of creation

    Cart() {
        created++;
        no = created;
    }

    public int no() {
        return no;
    }
}

@Component
class OrderService {                  // singleton, because no scope is specified
    private final Cart cart;

    OrderService(Cart cart) {         // injection happens only once, when OrderService is created
        this.cart = cart;
    }

    int cartNo() {
        return cart.no();
    }
}

@RestController
class OrderController {
    private final OrderService orderService;

    OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping("/order")
    String order() {
        int first = orderService.cartNo();
        int second = orderService.cartNo();   // used twice within one request
        return first + "," + second;
    }
}

Assumptions of this model

All 9 combinations were run on Spring Boot 4.1.1 (Spring Framework 7.0.9, embedded Tomcat 11) on this PC and checked against the results. For each combination, source code with the same structure as on the screen was compiled, the server was started, and two real GET /order requests were sent to read the Cart numbers in the responses. These were compared: the number of times the constructor of Cart was called (right after startup and after the two requests), what the field of OrderService holds (a Cart, an ObjectProvider, or a proxy), and the exception chain of the startup failure (UnsatisfiedDependencyException → UnsatisfiedDependencyException → ScopeNotActiveException → IllegalStateException). The second request was sent after the first one had finished (the "Singleton beans and state" lab covers requests sent at the same time). The following were also confirmed by execution: when two singleton beans have a prototype bean injected, they receive different objects; when a proxy is used with a prototype bean, a value stored with add is not visible in the size call right after it (the result is 0); a bean annotated with @RequestScope starts up even when it is injected directly and has one object per request; and the @PreDestroy method of a request-scoped bean is called each time a request ends. Not covered: the session scope and the application scope, other ways to retrieve a prototype bean (@Lookup, jakarta.inject.Provider), and the proxy mode that uses interfaces (ScopedProxyMode.INTERFACES). The Cart on the screen uses a static variable to count the objects, but real code that receives requests at the same time must not count this way.

Previous lab: Singleton beans and state · Next lab: Bean lifecycle callbacks

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these