skip to content

As an architect, how do you decide between singleton and prototype (and its provider/proxy plumbing), and what are the concurrency, memory, and testing trade-offs at scale?

level: principalimportance: nice to knowfreq 35%

answer

  1. default = stateless singleton
  2. prototype is a smell to justify
  3. prefer method params / plain new over scope
  4. ObjectProvider for testability, never getBean in domain
  5. concurrency risk = mutable singleton, not prototype

basics

~20 s

Default to stateless singletons — they're cheap, cached, and thread-safe when immutable. Reach for prototype only when you truly need per-use mutable state or non-shareable objects, and inject them via ObjectProvider. Prefer passing state as method parameters over creating stateful beans.

solid answer

~50 s

My default is stateless singletons: one cached instance, no per-request allocation, trivially thread-safe when immutable — the right shape for services, repositories, and controllers. I treat prototype scope as a smell to justify: it's warranted only when an object legitimately carries per-operation mutable state that must not be shared (stateful builders, per-attempt command objects) and where passing state as method arguments or using a local `new` isn't cleaner. When I do use prototype, I inject via `ObjectProvider<T>` for explicit, testable per-use resolution, avoid scoped proxies unless I need the request/session pattern, and make prototypes `AutoCloseable` so cleanup is deterministic. Concurrency risk lives in *singletons with mutable fields*, not prototypes. At scale I watch prototype allocation churn/GC pressure, ensure eager singleton wiring surfaces errors at startup, and keep container coupling out of business code (no getBean). Often the best answer is 'no bean at all' — a plain object or method parameter.

code

java · 17 lines
java
// Preferred: stateless singleton, state passed in — no scope plumbing
@Service
class PricingService {
    BigDecimal price(Cart cart) { /* pure, thread-safe */ return ...; }
}

// Justified prototype: stateful, per-attempt, AutoCloseable, provider-injected
@Service
class CheckoutFlow {
    private final ObjectProvider<PaymentAttempt> attempts;
    CheckoutFlow(ObjectProvider<PaymentAttempt> attempts) { this.attempts = attempts; }
    void checkout(Order o) {
        try (PaymentAttempt a = attempts.getObject()) {   // fresh + cleaned up
            a.process(o);
        }
    }
}

go deeper

for a junior

Know the default is singleton and prototype is the rare exception.

for a middle

Articulate 'stateless singleton by default, prototype only for per-use mutable state.'

for a senior

Compare wiring mechanisms and note that concurrency risk is mutable singletons, plus prototype allocation cost.

for a principal

Frame it as a state-ownership and lifecycle design decision; weigh testability, GC, startup validation, and prefer 'no bean / method parameter' over introducing scope machinery.

## The decision framework **Start from statelessness.** The overwhelming majority of application beans are stateless collaborators — services, DAOs/repositories, mappers, controllers, config holders. These should be **singletons** (the default): created once, cached, shared, and — because they hold no mutable per-call state — safe under concurrency. Singletons also give **eager startup validation**: wiring/config errors blow up at context refresh, not at first request. **Justify every prototype.** Prototype scope solves a narrow problem: an object that must **not** be shared because it holds **per-use mutable state** or represents a distinct short-lived unit of work (a stateful `Builder`, a per-attempt `Command`, an accumulator). Before choosing prototype, ask: 1. *Can the state be a method parameter instead?* Often yes — keep the bean a stateless singleton and pass state in. This is usually the cleanest design and sidesteps all scope plumbing. 2. *Can I just `new` the object?* If it has no Spring-managed dependencies, a plain constructor call is simpler than a container-managed prototype. 3. *Does it truly need container wiring per instance?* If it needs injected collaborators AND fresh-per-use semantics, that's the real prototype use case. ## Wiring choices and their trade-offs - **`ObjectProvider<T>`** — idiomatic default. Injected once, `getObject()` per use, no CGLIB subclassing of the owner, easy to fake in tests (pass a lambda-backed provider). Also handles optional/multiple beans (`getIfAvailable`, `stream`). - **JSR-330 `Provider<T>`** — portable standard, fewer features; fine if you value spec portability. - **`@Lookup`** — abstract-method style; requires CGLIB subclass, so no `final` class/method, and the bean becomes non-instantiable directly (harder to unit-test without Spring). - **Scoped proxy (`proxyMode = TARGET_CLASS`)** — great for **request/session** scopes injected into singletons; for **prototype** it creates a new target *per method call*, which surprises people expecting one instance per logical operation. Adds a proxy indirection. - **`ApplicationContextAware` + `getBean`** — service-locator anti-pattern: couples code to the container, hurts testability. Avoid in business logic. ## Concurrency The subtle point: **prototype scope is not a concurrency tool.** Thread-safety problems come from **singletons with mutable, unsynchronized fields**, since one instance is shared across all threads. Options: keep singletons immutable/stateless, use `ThreadLocal` deliberately (with cleanup), or push mutable state into a prototype/request-scoped object or method-local variables. Making something prototype 'to be safe' when it's injected into a singleton field actually does nothing (the field is resolved once). ## Memory / performance at scale - Singletons: near-zero per-use allocation; watch for **accidental large caches** and classloader retention. - Prototypes: **one allocation + full wiring per request** — under high throughput this is GC churn and CPU (post-processors run each time). Profile before adopting prototype in a hot path; a pooled or parameter-passing design may be better. - Scoped proxies add per-call proxy dispatch overhead. ## Testing - Singletons: easy to unit-test in isolation; watch for state bleeding across tests if they're accidentally stateful. - `ObjectProvider`/`Provider`: trivially mockable (`() -> new Task()`), so business logic stays testable without a container. - `@Lookup`/scoped proxies: require Spring to materialize behavior, making pure unit tests harder — a mark against them for testability. ## Rules of thumb 1. Default singleton + stateless. 2. Prefer method parameters or plain `new` over introducing scope. 3. If prototype, inject via `ObjectProvider` and make it `AutoCloseable`. 4. Reserve scoped proxies for web scopes. 5. Never smuggle the container into domain logic via `getBean`. 6. Remember prototype has no destroy callback — own the cleanup.

  • A teammate makes a bean prototype 'to avoid concurrency issues,' but it's @Autowired into a singleton field. What do you tell them?
    It won't help: the field is resolved once at the singleton's creation, so all threads still share that one prototype instance. If the concern is shared mutable state, either keep it stateless, pass state as method arguments, or resolve a fresh instance per use via ObjectProvider — and identify which fields are actually unsafe.
  • When would you pick a request-scoped bean over a prototype for per-operation state in a web app?
    When the 'operation' maps to an HTTP request and you want the same instance shared across all collaborators within that request (and cleaned up at request end). Request scope gives one instance per request with proper destruction callbacks via a scoped proxy; prototype gives a new instance per lookup with no destruction management.

saying these in an interview costs you the question

  • Treating prototype scope as a fix for thread-safety.
  • Introducing prototype + provider plumbing when a method parameter or plain new would do.
  • Using ApplicationContext.getBean in domain logic.
  • Ignoring GC/allocation cost of prototypes in hot paths.
  • Forgetting prototypes have no destruction callback.

context