Dependency Injection & Bean Lifecycle

Bean Scopes

18 min Lesson 5 of 13

Bean Scopes

Every Spring bean has a scope — a rule that governs how many instances Spring creates and how long each instance lives. Choosing the wrong scope is one of the most common sources of subtle bugs in Spring applications, so understanding each scope deeply is non-negotiable for a working developer.

The Six Built-In Scopes

Spring ships with six built-in scopes. Two are always available (singleton and prototype); four require a web-aware ApplicationContext (request, session, application, and websocket). This lesson covers the four you will encounter daily.

Singleton — The Default Scope

When no scope is declared Spring uses singleton: the container creates exactly one instance of the bean per ApplicationContext, caches it, and injects that same reference into every class that depends on it. The instance is created when the context starts (eager initialisation) and destroyed when the context closes.

import org.springframework.stereotype.Service; // No @Scope annotation — singleton is the default @Service public class ExchangeRateService { private double eurToUsd = 1.08; // shared state — every caller sees this public double convert(double euros) { return euros * eurToUsd; } public void updateRate(double rate) { this.eurToUsd = rate; // mutates shared state } }
Mutable singleton state is a concurrency hazard. Because the same object is shared across all threads, unsynchronised fields like eurToUsd above can be read and written simultaneously. Design singletons to be stateless or effectively immutable — or synchronise explicitly. Most services (repositories, HTTP clients, calculators) are naturally stateless and are excellent singletons.

You can also declare scope explicitly for clarity:

import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Service; @Service @Scope("singleton") // identical to the default; written here for documentation public class ExchangeRateService { ... }

Prototype — A New Instance Every Time

With prototype scope Spring creates a brand-new instance every time the bean is requested — whether via applicationContext.getBean() or via injection. Spring never caches prototype instances and never calls their @PreDestroy method; lifecycle cleanup is the caller's responsibility.

import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @Component @Scope("prototype") public class CsvReportBuilder { private final StringBuilder buffer = new StringBuilder(); public CsvReportBuilder appendRow(String... columns) { buffer.append(String.join(",", columns)).append("\n"); return this; } public String build() { return buffer.toString(); } }

Each call to getBean(CsvReportBuilder.class) (or each injection point that requests one) receives its own clean buffer. Use prototype for beans that carry per-operation mutable state — builders, command objects, parsers — where sharing a single instance would cause data corruption.

Injecting a prototype into a singleton is a classic pitfall. When Spring injects a prototype into a singleton field it does so once at startup, so the singleton always holds the same prototype instance — defeating the purpose. The fix is to inject an ApplicationContext or use a lookup method (@Lookup) so the singleton fetches a fresh prototype on each use. You will see this pattern in the next lesson.

Request Scope — One Instance Per HTTP Request

In a web application the request scope creates one bean instance for the lifetime of a single HTTP request. The instance is created when the request arrives and discarded when the response is committed. This is safe to inject into singletons via a scoped proxy (Spring substitutes a proxy at compile time; at runtime the proxy delegates to the request-specific instance).

import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.stereotype.Component; import org.springframework.web.context.WebApplicationContext; @Component @Scope( value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS ) public class RequestContext { private String correlationId; private String authenticatedUser; public void setCorrelationId(String id) { this.correlationId = id; } public String getCorrelationId() { return correlationId; } public void setAuthenticatedUser(String u) { this.authenticatedUser = u; } public String getAuthenticatedUser() { return authenticatedUser; } }

A singleton filter or interceptor populates RequestContext at the start of each request, and any downstream service can inject it to read the current user or correlation ID — without passing parameters through every method call. This is a clean alternative to ThreadLocal.

Always set proxyMode = ScopedProxyMode.TARGET_CLASS when injecting a request- or session-scoped bean into a singleton. Without a proxy, Spring cannot create the singleton because the request-scoped bean does not yet exist at context startup — you will get a BeanCreationException.

Session Scope — One Instance Per HTTP Session

The session scope binds one bean instance to an HTTP session (the session created by the servlet container, identified by a cookie or URL rewrite). The instance persists across multiple requests from the same user until the session expires or is invalidated.

import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.stereotype.Component; import org.springframework.web.context.WebApplicationContext; import java.util.ArrayList; import java.util.List; @Component @Scope( value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS ) public class ShoppingCart { private final List<CartItem> items = new ArrayList<>(); public void add(CartItem item) { items.add(item); } public void remove(CartItem item){ items.remove(item); } public List<CartItem> getItems() { return List.copyOf(items); } public int size() { return items.size(); } }

The controller that handles POST /cart/add injects ShoppingCart as a normal dependency. Spring's proxy transparently routes each call to the cart belonging to the current user's session.

Session-scoped beans must be serialisable in clustered environments. When sessions are replicated across nodes (e.g., Redis session storage), the bean is serialised and deserialised. Implement java.io.Serializable and add a serialVersionUID if your application will ever run behind a load balancer.

Choosing the Right Scope — Decision Guide

  • Stateless services, repositories, configuration, HTTP clientssingleton. Thread-safe by design, cheapest option.
  • Stateful per-operation objects (builders, parsers, command objects) → prototype. Each caller gets a clean slate.
  • Per-request cross-cutting data (correlation ID, audit context, current tenant) → request with a scoped proxy.
  • Per-user state across multiple requests (shopping cart, wizard step, user preferences) → session with a scoped proxy.

Scoped Proxy Under the Hood

When you specify proxyMode = ScopedProxyMode.TARGET_CLASS, Spring uses CGLIB to generate a subclass of your bean at startup. That subclass is the actual singleton stored in the context. Every method call on it consults a ThreadLocal variable to locate the real instance bound to the current request or session, then forwards the call. The result: your singleton controller never holds a stale reference, and you write plain injection code without any threading boilerplate.

Summary

Bean scope determines instance lifetime. singleton is the right default for stateless collaborators; keep shared state out of singletons or synchronise it carefully. prototype delivers a fresh instance on each request and suits inherently stateful, short-lived objects. request and session scopes map Spring beans directly onto the HTTP request/session lifecycle and eliminate the need for ThreadLocal or manual session attribute management — but always pair them with ScopedProxyMode.TARGET_CLASS when injecting into singletons.