Backend Development 6 min read

Understanding Spring Controller Bean Scope: Singleton vs Prototype and Thread Safety

This article explains why Spring MVC controllers are singleton by default, demonstrates the thread‑safety issues caused by shared instance variables, shows how to test the behavior with sample code, and presents solutions such as using prototype scope, avoiding member variables, or employing ThreadLocal.

Top Architect
Top Architect
Top Architect
Understanding Spring Controller Bean Scope: Singleton vs Prototype and Thread Safety

In Spring MVC, a controller is a singleton bean by default, which means it is shared across all requests and is not thread‑safe if it holds mutable instance variables.

To illustrate the problem, the following code defines a controller with a non‑static int num field and two request mappings:

package com.riemann.springbootdemo.controller;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @author riemann
 * @date 2019/07/29 22:56
 */
@Controller
public class ScopeTestController {

    private int num = 0;

    @RequestMapping("/testScope")
    public void testScope() {
        System.out.println(++num);
    }

    @RequestMapping("/testScope2")
    public void testScope2() {
        System.out.println(++num);
    }

}

When the endpoint /testScope is called first, the output is 1 . A subsequent call to /testScope2 prints 2 , showing that the same controller instance is reused and the mutable field is shared, which is unsafe in a multithreaded environment.

To make the controller prototype‑scoped, the @Scope("prototype") annotation is added:

package com.riemann.springbootdemo.controller;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @author riemann
 * @date 2019/07/29 22:56
 */
@Controller
@Scope("prototype")
public class ScopeTestController {

    private int num = 0;

    @RequestMapping("/testScope")
    public void testScope() {
        System.out.println(++num);
    }

    @RequestMapping("/testScope2")
    public void testScope2() {
        System.out.println(++num);
    }

}

With prototype scope, each request receives a new controller instance. Calling /testScope prints 1 , and a subsequent call to /testScope2 also prints 1 , confirming that the state is not shared.

Solution

Do not define mutable member variables in a controller.

If a member variable is necessary, declare the controller with @Scope("prototype") to obtain a new instance per request.

Alternatively, use a ThreadLocal variable inside the controller to keep data isolated per thread.

Additional Explanation of Spring Bean Scopes

Spring defines five main bean scopes:

singleton : a single instance per Spring ApplicationContext . It is created eagerly unless lazy-init is used.

prototype : a new instance is created each time the bean is requested from the container; Spring does not manage its full lifecycle after creation.

request : a new instance for each HTTP request, specific to web applications.

session : a new instance for each HTTP session.

globalSession : a global session scope, similar to the servlet application scope.

JavaSpringThread SafetyControllersingletonPrototypeBean Scope
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.