skip to content

Why is heavy use of the Singleton pattern (a class exposing one globally reachable shared instance, typically via a static accessor) often treated as an anti-pattern?

level: middleimportance: must knowfreq 68%

answer

  1. two things: one instance + global access
  2. global access hides dependencies
  3. state leaks across tests, kills parallelism
  4. scope baked in: process vs tenant/request
  5. container singleton scope ≠ the anti-pattern

basics

~20 s

Because a singleton is global mutable state with a static access point. Callers reach it directly instead of declaring it, so dependencies are hidden, tests share and leak state between each other, substitution for fakes is hard, and lifetime/threading rules become implicit.

solid answer

~50 s

Singleton bundles two separate decisions: "there should be exactly one instance" and "anyone may reach it globally via a static accessor". The first is often legitimate; the second is what causes harm. Global access hides dependencies — a method signature says nothing about the five singletons it touches — so you cannot see coupling from the API, and you cannot substitute a test double without static mocking or reset hooks. Because the instance outlives individual operations, tests pollute each other and must run in a fixed order; parallel test execution breaks. Mutable singleton state also invites data races and hidden initialization-order rules ("call configure() before first use"), and it is usually per-process, which quietly breaks when the process becomes multi-tenant or multi-classloader. The fix is to keep the *single instance* (created once by a composition root or DI container) but inject it through constructors, so lifetime is a wiring decision and the dependency is explicit and replaceable.

code

pseudocode · 16 lines
pseudocode
// Harmful: dependency is invisible and unswappable
class OrderService {
  fun place(o: Order) {
    val now = Clock.getInstance().now()      // hidden global
    Metrics.getInstance().count("orders")    // hidden global
  }
}

// Better: one instance still, but injected and explicit
class OrderService(private val clock: Clock, private val metrics: Metrics) {
  fun place(o: Order) {
    val now = clock.now()
    metrics.count("orders")
  }
}
// main(): val svc = OrderService(SystemClock(), StatsdMetrics())  // created once

go deeper

for a junior

Say it is global state with a static access point; call out that tests can't easily replace it and shared state leaks between tests.

for a middle

Separate the two concerns (one instance vs global access), give the hidden-dependency and test-pollution arguments, and propose constructor injection with container singleton scope.

for a senior

Add threading/lazy-init hazards, initialization-order contracts, and scope evolution (process → tenant/request), plus a concrete incremental migration path off the static accessor.

for a principal

Discuss it as an architecture-governance issue: enforceable layering, parallel test throughput, multi-tenancy readiness, lifecycle ownership, and when the pragmatic cost of leaving legacy singletons in place is lower than migrating.

## What the pattern is The classic **Singleton** (from the Gang of Four catalogue) does two things at once: 1. **Cardinality guarantee** — the type ensures at most one instance exists. 2. **Global access point** — that instance is reachable from anywhere via a static call, e.g. `Config.getInstance()`. Typical shape (pseudocode): ``` class Config: private static instance = null private Config() { ... } static getInstance(): if instance == null: instance = new Config() // lazy init return instance ``` Most of the criticism targets point 2, not point 1. ## Why global access is the problem ### 1. Hidden dependencies With injection, `OrderService(clock, pricing, repo)` advertises exactly what it needs. With singletons, `OrderService()` may still touch `Clock.getInstance()`, `FeatureFlags.getInstance()` and `Metrics.getInstance()` deep inside a method. The dependency graph is real but invisible — you can only find it by reading every line. This defeats the purpose of dependency inversion and makes architectural rules ("the domain layer must not touch HTTP") unenforceable by signature or by tooling. ### 2. Testability damage - **Substitution**: to test with a fake clock you must either add a static setter (`Clock.setInstance(fake)`), use a static-mocking library, or reach for classloader tricks. All are more fragile than passing an argument. - **Test pollution**: the instance survives between tests, so test A's mutation is visible to test B. You get order-dependent, flaky suites and the dreaded `reset()` method that must be called in teardown — and is eventually forgotten. - **Parallelism**: shared mutable global state prevents running tests concurrently in one process. - **Setup cost**: a singleton often initializes eagerly and touches the network, files, or a database at first use, so a "unit" test suddenly needs infrastructure. ### 3. Concurrency and initialization hazards Lazy `getInstance()` is a classic data-race site; naive double-checked locking is broken without proper memory-visibility guarantees in several languages. Mutable singleton state accessed from many threads needs synchronization the callers cannot see. And initialization order becomes an implicit contract: "you must call `configure()` before anything reads it" is a rule enforced only by runtime explosions. ### 4. Wrong scope, discovered late "Exactly one" usually means "one per process". Reality later demands one per tenant, per request, per test, per region, or per plugin classloader. Because the scope is baked into the type via a static field, changing it means touching every call site. Injection keeps scope as a wiring decision in one place. ### 5. Lifecycle opacity Who creates it? When is it disposed? A singleton has no visible owner, so shutdown, reconfiguration, and resource release (connection pools, file handles) have no natural home. ## When a singleton is fine - **Stateless and pure**: a math helper, a formatter with no configuration, an enum-based constant. - **Immutable values** computed once. - **A genuine hardware/process-wide resource** with real "exactly one" semantics. - **Bootstrapping / composition root**: something has to be the entry point that wires everything else; it is allowed to be reachable globally *once*, so long as the rest of the code receives its collaborators. - Small scripts and short-lived programs, where the coordination costs never arrive. The practical test: *is it mutable, and does business logic reach for it directly?* If yes on both, it is the harmful kind. ## The recommended alternative Keep the single instance, drop the global access: - Create one instance in the **composition root** (main / the DI container's configuration). - Register it with **singleton lifetime/scope** in the container. - **Inject** it via constructors everywhere else. You keep the one-instance efficiency, and you gain explicit dependencies, per-test substitution, per-tenant or per-request rescoping without call-site changes, and a clear owner for lifecycle. Note that a DI container's "singleton scope" is *not* the Singleton anti-pattern: the difference is exactly that the object is handed to collaborators rather than fetched from a global. ## Unwinding an existing singleton 1. Extract an interface for what callers actually use. 2. Add a constructor parameter defaulting to the existing `getInstance()` so nothing breaks. 3. Migrate call sites to injection incrementally; new code never calls the static accessor. 4. Delete the static accessor and let the compiler find stragglers. 5. Add a lint/architecture rule banning new static accessors.

  • Is registering a bean as a singleton in a DI container the same anti-pattern?
    No. The container creates one instance but *hands* it to collaborators through constructors. Dependencies stay explicit and replaceable per test or per environment; only the lifetime is shared. The anti-pattern is the static global access point, not the count of instances.
  • Give a case where a plain singleton is acceptable.
    Stateless, immutable helpers (a formatter, an enum constant), a genuinely process-wide hardware resource, or the composition root itself. The harmful combination is mutable state plus business code fetching it directly from a static accessor.

A single office printer everyone can use is fine. The problem is when staff walk to it silently instead of the job ticket listing 'requires printer' — you cannot tell who depends on it, and you cannot swap in a spare for a rehearsal without affecting everyone.

saying these in an interview costs you the question

  • "Singletons are always evil" — stateless/immutable ones and composition roots are fine
  • Conflating DI-container singleton scope with the global-static anti-pattern
  • "We add a static setter for tests" — that is a symptom, not a fix
  • Claiming naive double-checked locking is safe in every language
  • Assuming 'exactly one' will always mean 'one per process'

context