Have you ever added @Scope("prototype") to a Spring Boot Bean and been puzzled to see the same instance come back every time? Bean scopes tend to be left at singleton without much thought, but understanding them properly broadens your design options. This article walks through how the five standard scopes behave and the pitfalls of injecting a prototype into a singleton.

What is a Bean Scope

A Bean scope defines the lifetime and sharing unit of a Bean instance managed by the Spring container.

The default is singleton, meaning exactly one instance is created for the entire container. Singleton is the default because it is memory-efficient, and sharing is perfectly safe for a stateless service layer. Conversely, if you need to hold state, you should consider a different scope.

Scopes are specified with the @Scope annotation. For the basics of Beans, see What is @Component.

The Five Standard Scopes

Scope Quick Reference

Here is a summary of the purpose, lifetime, and typical use cases of each scope.

ScopeLifetimeNumber of instancesMain use casesproxyMode needed?
singletonEntire container1Stateless Service / RepositoryNo
prototypeEach retrievalOne per retrievalShort-lived, stateful processing objectsRequired when injecting into a singleton
requestHTTP requestOne per requestPer-request traceId / contextRequired (TARGET_CLASS)
sessionHTTP sessionOne per sessionLogged-in user info / shopping cartRequired (TARGET_CLASS)
applicationServletContext1Configuration shared across the whole appUsually not needed

Guideline for choosing: When in doubt, start with singleton. Consider a short-lived scope only when you need to hold state, and when injecting into a singleton, combine it with ObjectProvider or proxyMode = TARGET_CLASS.

Spring provides the following five scopes out of the box.

  • singleton: One per container. Ideal for a stateless service layer
  • prototype: A new instance on every retrieval. For short-lived, stateful objects
  • request: One per HTTP request (Web environments only)
  • session: One per HTTP session. Logged-in user information and the like
  • application: One per ServletContext. Configuration shared across all requests

The declaration looks like this.

@Component
@Scope("prototype")
public class OrderProcessor {
    // 取得のたびに新しいインスタンス
}

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private String traceId;
    // リクエスト単位の情報を保持
}

Using constants such as ConfigurableBeanFactory.SCOPE_PROTOTYPE instead of string literals helps prevent typos.

Choosing the Right proxyMode

When you inject a short-lived scoped Bean such as request or prototype into a singleton, you need to specify proxyMode. This is because the reference injected when the singleton is initialized becomes fixed and will not switch on a per-request basis.

There are two options for proxyMode.

  • TARGET_CLASS: A subclass proxy generated by CGLIB. Works even without an interface
  • INTERFACES: A JDK dynamic proxy. Requires the class to implement an interface

TARGET_CLASS is fine in most cases, but be aware that a subclass cannot be created for a final class.

The Pitfall of Injecting a Prototype into a Singleton

This is the point where people most often stumble. Take a 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() is the same. Since TaskRunner itself is a singleton, the reference to the Task received in the constructor is fixed. “A prototype gives you a new instance every time” applies to retrieval, and whatever was injected and held on to remains a single instance.

The recommended approach since Spring 4.3 is ObjectProvider. By calling getObject() at the moment you need it, you get a fresh 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 such as getIfAvailable() and getIfUnique() are also available, making it easy to work with.

Workaround 2: jakarta.inject.Provider

The JSR-330 standard Provider achieves the same thing. It is useful when you care about 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 it, 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 plugs in the method implementation for you.

@Service
public abstract class TaskRunner {
    @Lookup
    protected abstract Task createTask();

    public void run() {
        Task task = createTask();
        System.out.println("task id = " + task.getId());
    }
}

Because this relies on CGLIB subclass generation, it cannot be used with final classes. It is worth remembering as an option for situations where ObjectProvider cannot be used for some reason.

Workaround 4: Retrieving Directly from ApplicationContext

As a last resort, you can also 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 your logic to the Spring API and makes tests harder to write. Choose it only when no other approach is available.

Prerequisites for Using request / session Scopes

The request and session scopes are only valid in a Web environment (Spring MVC). When injecting them into a singleton controller or service, proxyMode = ScopedProxyMode.TARGET_CLASS is mandatory, as described 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 using them in tests, RequestContextHolder needs to be set up, so slice tests such as @WebMvcTest make things easier.

Watch Out for Anti-Patterns

Once you start paying attention to scopes, it is tempting to get fancy with your design, but the following patterns should be avoided.

  • Stateful singletons: Thread safety breaks down easily. Do not keep mutable state in fields
  • Making everything prototype: Over-engineering. Passing state through arguments is often simpler
  • Cramming lots of business logic into request/session Beans: Makes testing difficult. Keep them as holders of information

When in doubt, the safe choice is to consider a singleton plus passing state through arguments design first. The Bean lifecycle itself is covered in detail in Bean lifecycle.

Summary

The foundation of Bean scopes is singleton. When state management becomes necessary, you choose between prototype, request, and session. When injecting a short-lived Bean into a singleton, treating ObjectProvider as the first candidate leads to straightforward code. When using Web-dependent scopes, do not forget to specify proxyMode and set up the context during tests.

Understanding scopes lets you explain “why this design was chosen.” Even if singleton has never caused you trouble, knowing the alternatives adds more tools to your design toolbox.

If you want to dig deeper into the DI/Bean area of Spring Boot, the following articles are also worth reading.