Singleton
Purpose
Treat this pattern as a request to justify global state. Singleton bundles two separate
decisions — there is one instance and anyone can reach it without being given it — and the
second is what causes the damage. It hides dependencies from constructors, so a type's real
collaborators are invisible; it fixes initialisation order in ways nobody chose; it makes tests
order-dependent; and it silently promises a uniqueness that stops at the class loader.
Almost always the requirement is "one instance", and dependency injection delivers exactly that
by constructing one and wiring it. The instance is then unique because nothing else makes one —
without any type having to enforce it, and without any caller reaching around its constructor.
Inspect compiler/toolchain, container definitions and deployment topology first. Implementation
examples use Java 17 without preview; ScopedValue is final in Java 25 and represents dynamic
context binding, not instance uniqueness. Do not upgrade a project to adopt an idiom.
The uniqueness ladder
Thread binding ThreadLocal (not a uniqueness guarantee)
Dynamic scope ScopedValue (may share the same value across structured forks)
Defining class loader a static field — the same class may exist in several loaders
Process (JVM) a static field, if one class loader; a DI container's
singleton scope, if one relevant bean definition/container
Container/pod the process, restated — one JVM per pod by convention
Node an OS-coordinated lock/socket, with stale-owner and namespace handling
Cluster leader election or a distributed lock with a lease
Region the above, plus a consensus system that spans zones
System a protocol and authority boundary, not a language primitive
A conventional static getInstance() is bounded by the defining class loader. A requirement for
a horizontally scaled service — one scheduler, one cache warmer, one sequence generator, one
outbox relay — needs an explicit coordination/effect contract, and no amount of static will produce it. This is the
single most expensive misunderstanding in this pattern (leader-election,
distributed-locks-and-leases).
Spring singleton scope is one instance per bean definition per container, not per type/JVM.
Two definitions of the same class can produce two instances in one context; child contexts may
inherit a parent's bean or define their own. DI avoids global access only when callers actually
receive dependencies rather than consulting a static service locator.
When it is the answer
The type is a stateless, immutable value or function, and passing it
around is genuinely noise
→ an enum constant or a static final field. Not getInstance().
The hosting API owns creation and offers no injection point, while one
process-wide adapter must coordinate access to a JVM/native facility
→ a singleton bridge may be justified; hosting does not itself prove
uniqueness (ServiceLoader, for example, can return many providers).
A framework or legacy call site cannot be given a dependency and must
reach one
→ Singleton as a bridge, marked as such, with a plan to remove it.
When it is not
- "Configuration should exist once." It does — the container creates one and injects it. The
requirement was access, not uniqueness.
- "Creating it is expensive." That argues for creating it once, which is what a bean or a
field already does. It does not argue for reaching it statically.
- "Everything needs it." A dependency that everything needs is still a dependency; making it
invisible does not reduce coupling, it only stops the compiler from showing it.
- A cache or registry. Global mutable state under concurrency, with no eviction policy and
no owner. Give it an owner and inject it (
caching-strategies).
- Anything that must be unique across replicas. See the ladder above.
- Counters, sequence numbers, id generators. Process-local uniqueness produces colliding ids
the day a second replica starts.
Decision rules
IF the requirement is stated as "only one X"
THEN ask "one per what?" and place it on the ladder before designing.
IF the answer is cluster or system
THEN this pattern is irrelevant. Use leader election, a lease, or make
the operation idempotent so multiplicity stops mattering (idempotency).
IF the type has mutable state and is reached statically
THEN it is global mutable state. Every thread-safety argument must be
made explicitly, and every test must undo it.
IF a singleton is being added so that code can reach a collaborator
THEN pass the collaborator. The singleton is solving a plumbing problem
by removing the plumbing from view.
IF lazy initialisation is required
THEN use the holder idiom or an enum. Double-checked locking is correct
only with a volatile field and is rarely worth the risk.
IF the singleton's initialiser touches another class's static initialiser
THEN inspect cycles and blocking: cross-class initialization alone is normal,
but circular waits between initializing threads can deadlock. Avoid cyclic
initialization and keep fallible/blocking acquisition in an owned lifecycle.
IF tests need a reset() method on it
THEN treat that as evidence of hidden mutable lifetime. Prefer an owned instance;
when legacy migration requires reset, synchronize it, constrain it to tests,
and prevent parallel-test interference.
IF an enum is used purely as a namespace for one instance holding
mutable state
THEN the serialisation and reflection safety it buys is irrelevant, and
the global-state cost remains.
Cross-cutting checks
- Concurrency. Uniqueness and thread safety are unrelated: a singleton is one instance
shared by every thread, which makes any mutable field in it a contended, visibility-sensitive
variable. Publication of the instance itself must be safe — the holder idiom and
enum get
this from class-initialisation semantics; a plain if (instance == null) does not, and
double-checked locking without volatile has no Java Memory Model guarantee
(java-memory-model).
- Distribution. Process-local, always. A singleton connection pool, rate limiter or
scheduler becomes N of them under horizontal scaling, and the resulting limit is N times what
was configured — a common cause of exhausting a database's connection limit after a scale-up
(
connection-pool-sizing, rate-limiting-and-load-shedding).
- Performance. A contended
synchronized getInstance() on a hot path can add latency; modern
JVMs can make uncontended locking cheap, while the holder idiom removes per-access locking. The
larger effect is indirect: a single shared mutable
structure becomes the contention point for the whole application, and no amount of lock
tuning fixes a design that funnels every thread through one object
(false-sharing-and-contended, lock-inflation).
- Testing. Static state survives between tests in the same JVM, so tests pass alone and fail
in a suite, or pass in one order and fail in another. Parallel test execution makes it worse.
The absence of a constructor parameter also means a test cannot substitute the collaborator
through constructor injection; legacy seams, wrappers or isolated processes may help during
migration (
java-test-design).
Review checklist
Return the required scope, actual creation/call sites, owner and close/retry policy, chosen
mechanism and observed checks versus pending. Missing topology or external callers leaves
uniqueness and removal safety conditional.
References
- Uniqueness and scope — the ladder in full: what mechanism
provides uniqueness at each level, what defeats it (class loaders, multiple contexts, replicas,
restarts), and the distributed alternatives with their failure modes — leases expiring,
split-brain, and why idempotency often removes the requirement. Read whenever "there must be
only one" is stated.
- Implementation and migration — enum, holder
idiom and double-checked locking compared with their exact guarantees, the class-initialisation
deadlock, reflection and serialisation attacks on the invariant, and a step-by-step migration
off an entrenched singleton without a big-bang change. Read when implementing or removing one.
1---2name: gof-singleton3description: Singleton in modern Java, treated as a high-risk pattern: it conflates "one instance" with "reachable from anywhere", which must be justified separately. Covers why dependency injection gives uniqueness as a consequence of wiring, the scale ladder showing a Java singleton is unique per class loader and never per cluster, the safe lazy-initialisation idioms and the class-initialisation deadlock they invite, the static-state leakage that makes tests order-dependent, and the distributed mechanisms that give system-wide singularity. Use when getInstance() appears, when a scheduled job must run once across replicas, when someone says "singleton" meaning Spring's singleton scope, when tests pass alone and fail together, or when a cache or registry is being made global. Does not cover shared immutable instances for memory (gof-flyweight), wiring in general (java-dependency-inversion), cluster-wide leadership (leader-election), or once-only scheduling across replicas (distributed-locks-and-leases).4---56# Singleton78## Purpose910Treat this pattern as a request to justify global state. Singleton bundles two separate11decisions — _there is one instance_ and _anyone can reach it without being given it_ — and the12second is what causes the damage. It hides dependencies from constructors, so a type's real13collaborators are invisible; it fixes initialisation order in ways nobody chose; it makes tests14order-dependent; and it silently promises a uniqueness that stops at the class loader.1516Almost always the requirement is "one instance", and dependency injection delivers exactly that17by constructing one and wiring it. The instance is then unique because nothing else makes one —18without any type having to enforce it, and without any caller reaching around its constructor.1920Inspect compiler/toolchain, container definitions and deployment topology first. Implementation21examples use Java 17 without preview; ScopedValue is final in Java 25 and represents dynamic22context binding, not instance uniqueness. Do not upgrade a project to adopt an idiom.2324## The uniqueness ladder2526```text27Thread binding ThreadLocal (not a uniqueness guarantee)28Dynamic scope ScopedValue (may share the same value across structured forks)29Defining class loader a static field — the same class may exist in several loaders30Process (JVM) a static field, if one class loader; a DI container's31 singleton scope, if one relevant bean definition/container32Container/pod the process, restated — one JVM per pod by convention33Node an OS-coordinated lock/socket, with stale-owner and namespace handling34Cluster leader election or a distributed lock with a lease35Region the above, plus a consensus system that spans zones36System a protocol and authority boundary, not a language primitive37```3839A conventional static `getInstance()` is bounded by the defining class loader. A requirement for40a horizontally scaled service — one scheduler, one cache warmer, one sequence generator, one41outbox relay — needs an explicit coordination/effect contract, and no amount of `static` will produce it. This is the42single most expensive misunderstanding in this pattern (`leader-election`,43`distributed-locks-and-leases`).4445Spring singleton scope is one instance per bean definition per container, not per type/JVM.46Two definitions of the same class can produce two instances in one context; child contexts may47inherit a parent's bean or define their own. DI avoids global access only when callers actually48receive dependencies rather than consulting a static service locator.4950## When it is the answer5152```text53The type is a stateless, immutable value or function, and passing it54around is genuinely noise55 → an enum constant or a static final field. Not getInstance().5657The hosting API owns creation and offers no injection point, while one58process-wide adapter must coordinate access to a JVM/native facility59 → a singleton bridge may be justified; hosting does not itself prove60 uniqueness (ServiceLoader, for example, can return many providers).6162A framework or legacy call site cannot be given a dependency and must63reach one64 → Singleton as a bridge, marked as such, with a plan to remove it.65```6667## When it is not6869- **"Configuration should exist once."** It does — the container creates one and injects it. The70 requirement was access, not uniqueness.71- **"Creating it is expensive."** That argues for creating it once, which is what a bean or a72 field already does. It does not argue for reaching it statically.73- **"Everything needs it."** A dependency that everything needs is still a dependency; making it74 invisible does not reduce coupling, it only stops the compiler from showing it.75- **A cache or registry.** Global mutable state under concurrency, with no eviction policy and76 no owner. Give it an owner and inject it (`caching-strategies`).77- **Anything that must be unique across replicas.** See the ladder above.78- **Counters, sequence numbers, id generators.** Process-local uniqueness produces colliding ids79 the day a second replica starts.8081## Decision rules8283```text84IF the requirement is stated as "only one X"85THEN ask "one per what?" and place it on the ladder before designing.8687IF the answer is cluster or system88THEN this pattern is irrelevant. Use leader election, a lease, or make89 the operation idempotent so multiplicity stops mattering (idempotency).9091IF the type has mutable state and is reached statically92THEN it is global mutable state. Every thread-safety argument must be93 made explicitly, and every test must undo it.9495IF a singleton is being added so that code can reach a collaborator96THEN pass the collaborator. The singleton is solving a plumbing problem97 by removing the plumbing from view.9899IF lazy initialisation is required100THEN use the holder idiom or an enum. Double-checked locking is correct101 only with a volatile field and is rarely worth the risk.102103IF the singleton's initialiser touches another class's static initialiser104THEN inspect cycles and blocking: cross-class initialization alone is normal,105 but circular waits between initializing threads can deadlock. Avoid cyclic106 initialization and keep fallible/blocking acquisition in an owned lifecycle.107108IF tests need a reset() method on it109THEN treat that as evidence of hidden mutable lifetime. Prefer an owned instance;110 when legacy migration requires reset, synchronize it, constrain it to tests,111 and prevent parallel-test interference.112113IF an enum is used purely as a namespace for one instance holding114mutable state115THEN the serialisation and reflection safety it buys is irrelevant, and116 the global-state cost remains.117```118119## Cross-cutting checks120121- **Concurrency.** Uniqueness and thread safety are unrelated: a singleton is one instance122 shared by every thread, which makes any mutable field in it a contended, visibility-sensitive123 variable. Publication of the instance itself must be safe — the holder idiom and `enum` get124 this from class-initialisation semantics; a plain `if (instance == null)` does not, and125 double-checked locking without `volatile` has no Java Memory Model guarantee126 (`java-memory-model`).127- **Distribution.** Process-local, always. A singleton connection pool, rate limiter or128 scheduler becomes N of them under horizontal scaling, and the resulting limit is N times what129 was configured — a common cause of exhausting a database's connection limit after a scale-up130 (`connection-pool-sizing`, `rate-limiting-and-load-shedding`).131- **Performance.** A contended `synchronized getInstance()` on a hot path can add latency; modern132 JVMs can make uncontended locking cheap, while the holder idiom removes per-access locking. The133 larger effect is indirect: a single shared mutable134 structure becomes the contention point for the whole application, and no amount of lock135 tuning fixes a design that funnels every thread through one object136 (`false-sharing-and-contended`, `lock-inflation`).137- **Testing.** Static state survives between tests in the same JVM, so tests pass alone and fail138 in a suite, or pass in one order and fail in another. Parallel test execution makes it worse.139 The absence of a constructor parameter also means a test cannot substitute the collaborator140 through constructor injection; legacy seams, wrappers or isolated processes may help during141 migration (`java-test-design`).142143## Review checklist144145Return the required scope, actual creation/call sites, owner and close/retry policy, chosen146mechanism and observed checks versus pending. Missing topology or external callers leaves147uniqueness and removal safety conditional.148149- [ ] "One per what?" is answered explicitly and matches the mechanism used150- [ ] Nothing that must be unique across replicas relies on a static field151- [ ] The instance holds no mutable state, or every mutation is documented as thread-safe152- [ ] Lazy initialisation uses the holder idiom or an enum, not unguarded or non-volatile checks153- [ ] Initialization has no cyclic/blocking dependency and has an explicit failure policy154- [ ] Legacy resets are isolated from concurrent tests and tracked for removal155- [ ] Dependency injection was considered and rejected for a stated reason156- [ ] Spring's singleton scope is not described as this pattern in review comments157158## References159160- [Uniqueness and scope](references/uniqueness-and-scope.md) — the ladder in full: what mechanism161 provides uniqueness at each level, what defeats it (class loaders, multiple contexts, replicas,162 restarts), and the distributed alternatives with their failure modes — leases expiring,163 split-brain, and why idempotency often removes the requirement. Read whenever "there must be164 only one" is stated.165- [Implementation and migration](references/implementation-and-migration.md) — enum, holder166 idiom and double-checked locking compared with their exact guarantees, the class-initialisation167 deadlock, reflection and serialisation attacks on the invariant, and a step-by-step migration168 off an entrenched singleton without a big-bang change. Read when implementing or removing one.