# Gof Singleton

> 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).

- Skill: `robsonkades/gof-singleton` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add robsonkades/gof-singleton`
- Raw SKILL.md: https://api.skillmd.com/api/skills/robsonkades/gof-singleton/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: robsonkades (https://skillmd.com/u/robsonkades)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/robsonkades/gof-singleton

---


# 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

```text
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

```text
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

```text
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.

- [ ] "One per what?" is answered explicitly and matches the mechanism used
- [ ] Nothing that must be unique across replicas relies on a static field
- [ ] The instance holds no mutable state, or every mutation is documented as thread-safe
- [ ] Lazy initialisation uses the holder idiom or an enum, not unguarded or non-volatile checks
- [ ] Initialization has no cyclic/blocking dependency and has an explicit failure policy
- [ ] Legacy resets are isolated from concurrent tests and tracked for removal
- [ ] Dependency injection was considered and rejected for a stated reason
- [ ] Spring's singleton scope is not described as this pattern in review comments

## References

- [Uniqueness and scope](references/uniqueness-and-scope.md) — 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](references/implementation-and-migration.md) — 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.

