Bean scopes, and the prototype that stopped being one
Singleton, prototype and the web scopes — and why a prototype injected into a singleton is created exactly once.
After this lesson you can
- Name the bean scopes and say when each instance is created
- Explain why a prototype injected into a singleton behaves like a singleton
- Choose between ObjectProvider, a lookup method and a scoped proxy to fix it
A bean's scope decides how many instances the container makes and when. Two scopes exist in every Spring application:
- singleton (the default) — one instance per container, created at
startup and shared by every bean that injects it. "Singleton" here
means one per
ApplicationContext, not one per JVM: two contexts hold two instances. - prototype — a new instance every time the bean is requested,
whether by
getBeanor by being injected into another bean. The container creates and wires it, then hands it over; it does not track it, so@PreDestroyis never called for a prototype.
A web application adds request, session and application
scopes (one instance per HTTP request, per HTTP session, per
ServletContext), plus websocket.
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
class RequestCounter { private int count; int next() { return ++count; } }
@Service
class ReportService {
private final RequestCounter counter; // injected ONCE
ReportService(RequestCounter counter) { this.counter = counter; }
}
The trap: a prototype inside a singleton
Injection happens when the receiving bean is built. A singleton is built once, so its constructor runs once, so it asks for the prototype once. Every later call uses that same instance — the prototype scope has no effect at all from the singleton's point of view. The model below runs both versions: one service holds the prototype directly, the other holds a provider and asks the container for a fresh instance on every call.
import java.util.*;import java.util.function.Function;import java.util.function.Supplier; public class Solution { static final class Container { private final Map<String, String> scopes = new HashMap<>(); private final Map<String, Function<Container, Object>> factories = new HashMap<>(); private final Map<String, Object> singletons = new HashMap<>(); private int created = 0; void define(String name, String scope, Function<Container, Object> factory) { scopes.put(name, scope); factories.put(name, factory); } int nextId() { return ++created; } Object getBean(String name) { if (scopes.get(name).equals("prototype")) return factories.get(name).apply(this); Object existing = singletons.get(name); if (existing != null) return existing; Object bean = factories.get(name).apply(this); singletons.put(name, bean); return bean; } } static final class Counter { final int id; int count; Counter(int id) { this.id = id; } String next() { return "counter#" + id + " count=" + (++count); } } static final class DirectService { final Counter counter; DirectService(Counter counter) { this.counter = counter; } String handle() { return counter.next(); } } static final class ProviderService { final Supplier<Counter> counters; ProviderService(Supplier<Counter> counters) { this.counters = counters; } String handle() { return counters.get().next(); } } public static Map<String, Object> run() { Container ctx = new Container(); ctx.define("counter", "prototype", c -> new Counter(c.nextId())); ctx.define("direct", "singleton", c -> new DirectService((Counter) c.getBean("counter"))); ctx.define("provider", "singleton", c -> new ProviderService(() -> (Counter) c.getBean("counter"))); DirectService direct = (DirectService) ctx.getBean("direct"); ProviderService provider = (ProviderService) ctx.getBean("provider"); Map<String, Object> out = new LinkedHashMap<>(); out.put("direct", List.of(direct.handle(), direct.handle(), direct.handle())); out.put("provider", List.of(provider.handle(), provider.handle(), provider.handle())); out.put("singleton same instance", ctx.getBean("direct") == ctx.getBean("direct")); out.put("prototype same instance", ctx.getBean("counter") == ctx.getBean("counter")); return out; }}The real fixes all do what the provider does — defer the lookup to call time:
- Inject
ObjectProvider<RequestCounter>and callgetObject()where you need an instance. - Declare an abstract or stub method annotated
@Lookup; Spring overrides it to return a fresh bean on each call. - Give the prototype a scoped proxy
(
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)). The singleton receives a proxy, and the proxy fetches a new target for every method call — which also means two calls on "the same" field hit two different objects, a surprise of its own.
Request- and session-scoped beans have the same problem in reverse: a
singleton is created before any request exists. That is why
@RequestScope and @SessionScope default to a target-class proxy —
the singleton gets a proxy that resolves the current request's
instance on each call.
Singletons are shared between threads
Every request thread in a web application calls the same singleton
instance. A mutable field on a singleton (private int count;, a
non-thread-safe SimpleDateFormat, a "current user" field) is shared
state with a race condition. Keep singletons stateless, put per-call
data in local variables, and reach for a narrower scope only when the
state genuinely belongs to one request or session.
Common mistakes
- Marking a bean
prototypeand injecting it into a singleton constructor, expecting a new instance per call — the example shows it is created once. - Storing per-request data (the current user, a running total) in a field of a singleton , so concurrent requests overwrite each other.
Try it yourself
2 visible tests · 3 hidden testsModel how many instances each bean ends up with. Implement
instancesCreated(Map<String, String> scopes, Map<String, List<String>> dependencies, List<String> requests).
scopes maps every bean name to "singleton" or "prototype";
dependencies maps every bean to the beans its constructor needs
(there are no cycles); requests is a list of getBean calls made
after startup.
Rules: at startup every singleton is created (in alphabetical order of name, though the counts do not depend on it). Creating any bean first obtains each of its dependencies: a singleton dependency is created only if it does not exist yet and is otherwise reused; a prototype dependency is a brand-new instance every time. After startup, each request for a singleton returns the existing instance; each request for a prototype creates a new instance (and obtains its dependencies the same way).
Return a map from every bean name in scopes to the number of
instances created, sorted by bean name (use a TreeMap). A bean that
was never created maps to 0.
instancesCreated({"counter":"prototype","service":"singleton"}, {"counter":[],"service":["counter"]}, ["service","service","service"])instancesCreated({"command":"prototype"}, {"command":[]}, ["command","command"])
Sign up to run the tests, check the hidden ones and save your progress. Sign up