Singleton is one of the simplest GoF patterns and one of the first taught. At the same time, in practice it gets labeled as an anti-pattern more often than any other. The reason “fundamental” and “anti-pattern” land on the same pattern is not in the pattern itself. It is that Singleton bundles two intents into one.
Single-instance guarantee and global access. The moment the two are bundled, tight coupling and test difficulty follow. This is the same context the dependency-injection post covers when explaining why DI emerged.
GoF Intent
GoF defines Singleton as “ensure a class has only one instance and provide a global point of access to it.” Two guarantees, in one bundle.
- Single-instance guarantee — A lifecycle decision. There is a domain fact that only one instance of the resource should exist within the process.
- Global access — A dependency-expression decision. How the client obtains the instance (constructor injection / method call / global reference).
The two decisions are separate axes by nature. Singleton bundled them into one pattern to gain simplicity. That simplicity is also where the anti-pattern debate starts.
Language Implementations
Each language handles thread safety differently.
Java has four common implementations.
- Eager initialization — Created at class loading. Simplest, but holds memory even when unused.
- Lazy initialization — Created at first call. Requires manual thread safety.
- DCL (Double-Checked Locking) —
volatileplus two-stage null check for both thread safety and lazy init. Breaks subtly ifvolatileis missing. - Initialization-on-demand Holder — Uses an inner static class to leverage JVM’s class loading guarantees. Same effect as DCL without the subtlety.
Python uses two common approaches.
- Module — A module loads exactly once at import, making it a natural Singleton. The most common approach.
- Metaclass — Overrides
__call__to control instance creation. Useful when class hierarchies are deep.
TypeScript / JavaScript rely on the module system itself for single-instance guarantee. ESM’s module cache loads the same module exactly once, so exported objects naturally become Singletons. The class with static getInstance() pattern works too, but module export reads more naturally.
On thread safety, JVM-based languages (Java/Kotlin) are the trickiest; Python’s GIL and JavaScript’s single-threaded model remove much of the same concern.
Why It Becomes an Anti-Pattern
Singleton gets labeled as an anti-pattern for four reasons.
- Global state — State accessible from anywhere is hard to trace. A change in module A affects what module B sees. The scope of tracking widens to the entire codebase.
- Test difficulty — Unit tests should run in isolation. A global instance is shared across tests, so changes from one test affect another. Mocking is also awkward — callers reference the concrete class directly, leaving no interface seam to swap.
- Tight coupling — A call like
Logger.getInstance()means the caller knowsLoggeras a concrete class. Swapping the implementation (moving to a different logger library) touches every caller. - No lifecycle control — A Singleton is created at the first call and lives until the process ends. The caller cannot decide an explicit release or recreate point.
When the four work together, the codebase grows fragile to change over time. A change starting in one module can affect places that are hard to predict.
Contrast with DI
DI containers separate Singleton’s two intents.
The single-instance guarantee becomes a scope setting on the container. Spring’s @Scope("singleton") or NestJS’s default provider scope provide the same effect. The container creates one instance and injects the same one wherever the dependency is needed.
Global access is replaced by dependency injection. The client receives the dependency through a constructor parameter and does not know how it was obtained. Swapping implementations becomes a one-line container change, and tests inject mocks to create isolated environments.
Once the two intents are separated, the benefit of a single instance stays while the cost of global access disappears. DIP, covered in the dependency-injection post, is the abstraction that makes this separation possible.
Where It Still Fits
DI being the general alternative does not make Singleton anti in every case. Within a narrow range, it still fits.
- Thread pools, connection pools — Resources that must exist exactly once per process. Multiple pools create resource contention.
- Loggers — The output channel needs consistent management. Multiple instances scramble output order or format.
- Configuration loaders — Read once at process startup and held in memory. Multiple instances duplicate loading or break consistency.
- Caches — Sharing across the process is the point. A separate cache per instance defeats the cache itself.
The two common traits are: lifecycle equals process lifetime, and state is the essence. There is no reason to scale instances up or down, and the resource is meant to be shared. External injection adds complexity without benefit.
Even in these cases, expressing it through a DI container’s singleton scope is more flexible. Only in environments without a container (simple scripts, CLI tools, embedded) does direct Singleton implementation cost less.
Conclusion
Singleton itself is not the anti — the decision to bundle single-instance guarantee and global access into one pattern is. Separate the two intents and you keep the benefits of a single instance without the tight coupling and test difficulty. DI is the tool that generalizes that separation.
The choice comes down to two questions.
- Is a DI container available? If yes, expressing it through the container’s singleton scope separates the two intents and is the first choice.
- Is this one of those narrow cases where separation adds more cost? In environments without a container, when the lifecycle equals process lifetime and state is the essence, direct Singleton implementation reads naturally.
The simplest pattern, with the most nuanced application decision.
References
- Dependency Injection — The Hierarchy of DIP, IoC, and DI — Why DI is the alternative to Singleton
- Misko Hevery — Singletons are Pathological Liars
- GoF — Design Patterns: Elements of Reusable Object-Oriented Software (1994)