Hermes Wiki

Singleton Pattern

Concept

Singleton ensures a class has exactly one instance for the lifetime of the program and provides a single, globally reachable access point to it. The classic shape: a private constructor (so nothing outside the class can call new), a static field holding the one instance, and a static getInstance() method that lazily creates the instance on first call and returns the same instance on every call after that. It's one of the simplest GoF patterns to implement and, by wide consensus among practitioners today, one of the most overused — the pattern is frequently reached for as a shortcut to avoid passing a dependency through constructors, which trades a small amount of local convenience for global coupling that's expensive to unwind later.

The naive lazy implementation above is not thread-safe: two threads can both observe instance == null before either finishes construction, and both proceed to build (and briefly return) two different instances. Three standard fixes exist, each with a real cost. Eager initialization — constructing the instance at class-load time regardless of whether it's ever used — is trivially thread-safe (the language runtime's class-loading guarantees handle the race) but gives up laziness, which matters when the singleton is expensive to build and not always needed. A fully synchronized getInstance() is correct but pays a lock acquisition on every call for the entire life of the program, not just the first one where the race actually matters. Double-checked locking (DCL) tries to get both: check the field unsynchronized first, only enter a synchronized block (and re-check) if it looks null, so the lock is paid once. DCL has a well-known history of being subtly broken — in Java, prior to the memory-model fixes in Java 5, the JIT/CPU could reorder instructions such that another thread observed a non-null reference to a partially constructed object; the fix requires declaring the instance field volatile. Getting this detail wrong is exactly the kind of bug that "appears to work" in testing and fails intermittently in production under real concurrency.

Tradeoffs

Approach Benefit Cost
Hand-rolled Singleton (private constructor + static getInstance()) Guarantees exactly one instance with no framework dependency Global mutable state; hard-codes the dependency into every caller; hard to substitute a test double without reflection tricks
Eager static initialization Simple, thread-safe by construction, no locking logic to get wrong No laziness — pays construction cost even if the instance is never used; still has all of Singleton's coupling/testability problems
Double-checked locking Lazy and avoids paying a lock on every call after the first Easy to implement incorrectly (missing volatile in Java, equivalent memory-ordering pitfalls in other languages); genuinely subtle to review
DI container, singleton-scoped registration Same "exactly one instance" guarantee, but the class itself stays a plain constructor-injected class — trivially substitutable with a test double Requires a DI framework/container already in place in the codebase
Module-level instance (Python module import cache, Node.js require cache) The language/runtime already guarantees "loaded once" — no pattern code needed at all Still global state with the same testability/coupling issues; import-order and module-reload edge cases can violate the "once" guarantee in less common setups

When to use / when not to

  • Use sparingly, and only for something that must genuinely be singular and expensive/dangerous to duplicate — a database connection pool, a hardware resource handle, a process-wide cache client.
  • Prefer a dependency-injection container's singleton-scoped lifetime over a hand-rolled getInstance() in any codebase that already has DI available — it gets the same one-instance guarantee without hard-wiring the global-state coupling into the class itself.
  • Don't use it as a default way to avoid threading a dependency through constructors — that's the single most common misuse, and it's the source of most of the pattern's bad reputation.
  • Don't use it for anything that plausibly needs more than one instance later (per-tenant config, per-request state) — a Singleton locks that decision in structurally, and un-Singleton-ing a class that's been called from fifty places via a static reference is a much larger refactor than not introducing it in the first place.

Common pitfall

Treating Singleton as a convenience for avoiding constructor parameters rather than a deliberate constraint on the domain (only one of this thing should ever exist). The convenience use introduces global mutable state that's invisible in any class's constructor signature — a class that internally calls Database.getInstance() has a hidden dependency that isn't visible from its interface, can't be swapped for a test double without extra machinery (reflection, a test-only reset method, or a static-field-clearing hack between tests), and silently couples that class to every other class touching the same global instance. In test suites specifically, a Singleton that isn't reset between tests leaks state from one test into the next, producing order-dependent test failures that are painful to diagnose because the shared instance itself isn't visible in either failing test's own code.

Engineering Lens

In review, a hand-rolled getInstance() is often the smell worth flagging rather than the fix — the question to ask is whether the codebase already has (or could reasonably add) dependency injection, in which case a constructor-injected, container-managed singleton-scoped instance gets the identical "exactly one instance" guarantee without the global-state and testability costs. Where DI genuinely isn't available (a small script, a language/runtime without established DI conventions), Singleton remains a legitimate, narrow tool — but the bar should be "this really must be globally singular," not "this saves me from passing an argument."

  • Builder Pattern — both are creational patterns, but Builder solves a construction-readability problem while Singleton solves a cardinality constraint

Sources

Hermes Wiki