Have you been frustrated by Spring Boot’s behavior where adding @Scope("prototype") still returns the same instance every time? Bean scopes tend to be left as singleton by default, but understanding them properly broadens your design options. In this article, we’ll organize the behavior of the five standard scopes and the pitfalls of injecting prototype into singleton.
What Are Bean Scopes?
Bean scopes define the lifetime and sharing unit of Bean instances managed by the Spring container.
The default is singleton, where only one instance is created for the entire container. Singleton is the default because it’s memory-efficient and there’s no problem sharing stateless service-layer beans. Conversely, if you need to hold state, you should consider another scope.
Scopes are specified with the @Scope annotation. For Bean basics, refer to What is @Component.
The Five Standard Scopes
Spring provides the following five standard scopes:
- singleton: One per container. Ideal for stateless service layers
- prototype: New instance on each retrieval. For short-lived, stateful objects
- request: Per HTTP request (Web environment only)
- session: Per HTTP session. Used for logged-in user information, etc.
- application: Per ServletContext. For configuration shared across all requests
Declarations look like this:
@Component
@Scope("prototype")
public class OrderProcessor {
// 取得のたびに新しいインスタンス
}
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String traceId;
// リクエスト単位の情報を保持
}
Using constants like ConfigurableBeanFactory.SCOPE_PROTOTYPE instead of string literals helps prevent typos.
Using proxyMode
When injecting short-lived scoped beans like request or prototype into singletons, you need to specify proxyMode. This is because the reference injected at singleton initialization gets fixed, so it doesn’t switch per request.
There are two options for proxyMode:
TARGET_CLASS: Subclass proxy via CGLIB. Works without an interfaceINTERFACES: JDK dynamic proxy. Requires an interface implementation
Basically TARGET_CLASS is fine, but note that subclasses cannot be created for final classes.
Pitfall: Injecting Prototype into a Singleton
This is the most common stumbling block. Look at the following code:
@Component
@Scope("prototype")
public class Task {
private final long id = System.nanoTime();
public long getId() { return id; }
}
@Service
public class TaskRunner {
private final Task task;
public TaskRunner(Task task) {
this.task = task;
}
public void run() {
System.out.println("task id = " + task.getId());
}
}
Even if you call TaskRunner twice, the value of task.getId() will be the same. Since TaskRunner itself is a singleton, the reference to Task received in the constructor is fixed. “Prototype means a new one each time” only applies at retrieval time—once injected and held, it remains a single instance.
Workaround 1: ObjectProvider (Recommended)
From Spring 4.3 onwards, ObjectProvider is the recommended approach. By calling getObject() when needed, you can get a new instance each time.
@Service
public class TaskRunner {
private final ObjectProvider<Task> taskProvider;
public TaskRunner(ObjectProvider<Task> taskProvider) {
this.taskProvider = taskProvider;
}
public void run() {
Task task = taskProvider.getObject();
System.out.println("task id = " + task.getId());
}
}
Null-safe APIs like getIfAvailable() and getIfUnique() are also available, making it easy to use.
Workaround 2: jakarta.inject.Provider
The JSR-330 standard Provider can do the same thing. This is useful when considering portability to other DI containers.
import jakarta.inject.Provider;
@Service
public class TaskRunner {
private final Provider<Task> taskProvider;
public TaskRunner(Provider<Task> taskProvider) {
this.taskProvider = taskProvider;
}
public void run() {
System.out.println("task id = " + taskProvider.get().getId());
}
}
To use this, you need to add a dependency on jakarta.inject:jakarta.inject-api.
Workaround 3: @Lookup Method Injection
If you annotate an abstract or concrete method with @Lookup, Spring generates a subclass and injects the method implementation.
@Service
public abstract class TaskRunner {
@Lookup
protected abstract Task createTask();
public void run() {
Task task = createTask();
System.out.println("task id = " + task.getId());
}
}
Since it relies on CGLIB subclass generation, it cannot be used with final classes. It’s worth keeping in mind as an option when ObjectProvider cannot be used.
Workaround 4: Direct Retrieval from ApplicationContext
As a last resort, you can use ApplicationContext.getBean.
@Service
public class TaskRunner {
private final ApplicationContext context;
public TaskRunner(ApplicationContext context) {
this.context = context;
}
public void run() {
Task task = context.getBean(Task.class);
System.out.println("task id = " + task.getId());
}
}
This tightly couples the logic to the Spring API, making it harder to test. Choose this only when no other means are available.
Prerequisites for request / session Scopes
request and session are only valid in Web environments (Spring MVC). When injecting into singleton controllers or services, proxyMode = ScopedProxyMode.TARGET_CLASS is required, as mentioned above.
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class LoginUser implements Serializable {
private String userId;
// セッション内で保持する情報
}
Making session-scoped beans Serializable prepares them for session replication and file persistence. When testing, you need to set up RequestContextHolder, so leveraging slice tests like @WebMvcTest makes things easier.
Beware of Anti-patterns
Once you start being conscious of scopes, you may be tempted to over-engineer, but the following patterns should be avoided:
- Stateful singletons: Thread safety easily breaks down. Don’t hold mutable state in fields
- Prototype for everything: Over-engineering. It’s often simpler to pass state via arguments
- Stuffing business logic into request/session: Makes testing difficult. Keep them as information holders only
When in doubt, the safest approach is to first consider a singleton with state passed as arguments. For the Bean lifecycle itself, see Bean lifecycle for details.
Summary
The basis of Bean scopes is singleton. When state management becomes necessary, use prototype, request, or session as appropriate. When injecting short-lived beans into a singleton, ObjectProvider should be your first choice for clean code. When using Web-dependent scopes, don’t forget to specify proxyMode and to set up the context during testing.
Understanding scopes lets you explain “why this design.” Even if you don’t usually have problems with singleton, knowing the options expands your design repertoire.