Skip to content
thesarfo

Concept

Spring Fundamentals: Bean Scopes

Singleton vs prototype vs request-scoped beans, why injecting a request-scoped bean into a singleton fails at startup, and how ScopedProxyMode fixes it.

views 0

Bean scope refers to the lifecycle and visibility of a bean in the Spring context — it determines how many instances of a bean are created and how they’re shared.

By default, a bean’s scope is Singleton: a single instance is created and shared across the entire Spring container, so every request for the bean returns the same instance. You specify scope via XML configuration or annotations.

Prototype scope

A new instance is created every time the bean is requested from the container — useful when each use needs a fresh instance.

public interface NumberService {
public int getValue();
}
@Service
public class RandomNumberService implements NumberService {
private final int value;
public RandomNumberService() {
this.value = new Random().nextInt(1000);
}
public int getValue() {
return value;
}
}
@Controller
public class IndexController {
@Autowired
private NumberService numberService; // proxy
@GetMapping("/home")
public String indexAction(Model model) {
model.addAttribute("message", numberService.getValue());
return "index.html";
}
}

Every call to getValue() here returns the same value, because RandomNumberService’s constructor only ever runs once — the default Singleton scope. With a Prototype scope, the value would change on every call.

Request scope

A new instance is created per HTTP request — only available in a web-aware application context.

@Service
@Scope(WebApplicationContext.SCOPE_REQUEST)
public class RandomNumberService implements NumberService {
private final int value;
public RandomNumberService() {
this.value = new Random().nextInt(1000);
}
public int getValue() {
return value;
}
}

Running this fails at startup: IndexController is a singleton, but it’s trying to inject a request-scoped RandomNumberService. The Spring container can’t resolve injecting a bean that’s created and destroyed per HTTP request into a bean that lives for the entire application lifetime — a scope mismatch.

Spring can still bridge this — it just needs to go through a proxy. Set the proxyMode attribute:

@Service
@Scope(WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RandomNumberService implements NumberService {
private final int value;
public RandomNumberService() {
this.value = new Random().nextInt(1000);
}
public int getValue() {
return value;
}
}

Instead of injecting the bean value directly, Spring injects a proxy that manages the bean. When the application context starts, Spring creates a proxy for RandomNumberService instead of a direct instance, and injects that proxy into IndexController’s numberService field. When numberService.getValue() is called, the proxy intercepts the call, resolves the right RandomNumberService instance for the current scope (the current HTTP request, here), and delegates to it. Now every request to IndexController returns a different value.

ScopedProxyMode

Used to handle injecting a shorter-lived bean (request/session scope) into a longer-lived one (singleton scope). Three modes:

  1. TARGET_CLASS — the most common. Spring creates a CGLIB subclass proxy of the target bean to handle method calls. Typically used when injecting scoped beans into other beans.

    @Service
    @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
    public class RandomNumberService implements NumberService {
    // ...
    }

    Behind the scenes: Spring creates a CGLIB proxy subclass that intercepts method calls and delegates to the appropriate scoped instance.

  2. INTERFACES — Spring creates a JDK dynamic proxy implementing the same interfaces as the target bean. Use this when the target implements one or more interfaces and you prefer JDK dynamic proxies over CGLIB.

    @Service
    @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.INTERFACES)
    public class RandomNumberService implements NumberService {
    // ...
    }
  3. NO — no proxy is created (the default). Can lead to scope mismatch issues if used where scopes don’t already align, since direct instances get injected instead of a scope-aware proxy.

    @Service
    @Scope(value = WebApplicationContext.SCOPE_REQUEST)
    public class RandomNumberService implements NumberService {
    // ...
    }