Intent
Ensure a class has exactly one instance, and provide a single, well-known access point to it.
That one sentence is the whole pattern. The interesting part — and the reason this lesson is longer than the pattern deserves — is that Singleton is the most over-applied pattern in the Gang of Four catalog. A senior interviewer is not testing whether you can write a thread-safe Singleton (you can; it’s mechanical). They are testing whether you know when not to, and whether you can articulate the failure modes. We’ll do both.
The Problem It Solves
Some resources are genuinely singular by nature, and constructing more than one is either wasteful or incorrect:
- A connection pool to a database. Two pools means double the file descriptors and two competing sets of idle connections.
- A hardware access point — a printer spooler, a GPU command queue, an embedded device register map. The physical thing is one; modeling it as many is a lie.
- A process-wide configuration registry loaded once from disk/env at startup.
- An in-process metrics/counter aggregator that must sum across the whole process.
The naive fix is “just make one and pass it around.” That is, in fact, usually the correct fix (we’ll return to this). Singleton goes further: it makes the class itself enforce uniqueness and publish a global access point, so callers can’t accidentally create a second one and don’t have to thread the reference through every constructor.
The pattern bundles two distinct guarantees: (1) “only one instance exists” and (2) “everyone can reach it globally.” These are separable. The first is a legitimate invariant. The second is what creates most of the trouble. Keep them mentally distinct — half of good Singleton judgment is noticing you wanted only (1).
Singleton = single-instance invariant + global access point. The pattern’s bad reputation comes almost entirely from the global access half: it smuggles hidden dependencies and global mutable state into your design. If you only need single-instance, prefer creating one object and injecting it.
Structure
┌─────────────────────────────────┐
│ Singleton │
├─────────────────────────────────┤
│ - instance : Singleton (static)│◄────┐
├─────────────────────────────────┤ │ returns the
│ - Singleton() (private) │ │ same object
│ + getInstance() : Singleton ────┼─────┘ (static)
│ + businessMethod() : ... │
└─────────────────────────────────┘
Client ──── calls ───► Singleton.getInstance().businessMethod()
The defining structural moves:
- The constructor is private (or otherwise non-public), so no external code can
newit. - A static field holds the single instance.
- A static accessor (
getInstance) returns that field, lazily creating it on first call (or eagerly at load time).
Participants
| Participant | Role |
|---|---|
| Singleton | Declares the static accessor, owns the static instance field, hides its constructor, and carries the actual business behavior. |
| Client | Obtains the instance via the static accessor instead of constructing one. There is no separate “Client” class in the pattern — it’s whoever calls getInstance(). |
Note how thin this is: Singleton has essentially one collaborator type and no abstraction. That thinness is a smell we’ll cash out in the SOLID section.
A Concrete Example: a Process-Wide Connection Pool
Forget the textbook Logger. Here is a realistic case where the single-instance invariant is real: a database connection pool. Creating two pools is an actual bug (descriptor exhaustion, mismatched limits), so the class enforces uniqueness. We’ll show the thread-safe lazy form in each language, because that’s where the real engineering lives.
// TypeScript runs on a single-threaded event loop, so data races on
// the instance field are impossible. The hazard is *async re-entrancy*:
// two callers awaiting initialization concurrently. We guard with a
// cached init promise, the idiomatic JS analogue of double-checked locking.
class ConnectionPool {
private static instance: ConnectionPool | null = null;
private static initPromise: Promise<ConnectionPool> | null = null;
private connections: string[] = [];
// "private" constructor: TS enforces this at compile time.
private constructor(private readonly maxSize: number) {}
static async getInstance(): Promise<ConnectionPool> {
if (ConnectionPool.instance) return ConnectionPool.instance;
// Collapse concurrent initializers onto one promise.
if (!ConnectionPool.initPromise) {
ConnectionPool.initPromise = (async () => {
const pool = new ConnectionPool(10);
await pool.warmUp();
ConnectionPool.instance = pool;
return pool;
})();
}
return ConnectionPool.initPromise;
}
private async warmUp(): Promise<void> {
for (let i = 0; i < this.maxSize; i++) {
this.connections.push(`conn-${i}`);
}
}
acquire(): string {
const c = this.connections.pop();
if (!c) throw new Error("pool exhausted");
return c;
}
release(c: string): void { this.connections.push(c); }
}
// Usage
// const pool = await ConnectionPool.getInstance();
// const conn = pool.acquire();A taxonomy of thread-safe implementations
You should be able to name these and state their trade-offs on the spot:
| Strategy | Language home | How it’s safe | Cost |
|---|---|---|---|
| Eager init | Any (static field initialized at load) | Created before any thread races; no lock | Built even if never used; init-order issues across translation units |
| Double-checked locking (DCL) | Java/C#/C++ (with care) | First check lockless, second under lock | Subtle: in C++/Java needs volatile/atomic + memory fences to avoid publishing a half-constructed object. In CPython, DCL is mostly pointless — the GIL plus a plain lock already suffices. |
| Initialization-on-demand holder | JVM only | Nested static class loaded lazily by classloader; JVM guarantees safety | JVM-specific; no equivalent in C++/JS/Python, so it doesn’t appear in the tabs above |
| Meyers singleton | C++11+ | Function-local static; compiler-inserted guard | The right default in C++; watch destruction order at process exit |
| Module-level object | Python (and ES modules) | Module body runs once under the import lock | The right default in Python; cannot do lazy-with-args cleanly |
The initialization-on-demand holder idiom is named here for completeness because interviewers ask about it, but it is a JVM-only trick (it leans on the classloader’s lazy, thread-safe class initialization). The three languages in our CodeTabs have strictly better native answers — Meyers in C++, module-level in Python, cached init-promise in TS — so we don’t show holder code. If you’re in Java, it looks like: a private static class Holder { static final X INSTANCE = new X(); } whose INSTANCE is touched only by getInstance().
Naive double-checked locking is a classic concurrency bug. In Java before 5 and in C++ without atomics, instance = new Singleton() can be reordered so the pointer is published before the constructor finishes — a second thread sees a non-null but half-built object. Fixes: mark the field volatile (Java 5+) / use std::atomic with acquire-release (C++), or just sidestep the whole thing with eager init, the holder idiom, or the Meyers singleton. Note the asymmetry for Python: under CPython’s GIL there is no such publication-reordering hazard, so the DCL “fast path” buys almost nothing — a plain with lock: (or, better, a module-level object) is enough. Hand-rolled DCL in Python is mainly relevant to free-threaded / no-GIL 3.13+ builds. In an interview, say: “I’d avoid hand-rolled DCL and use the language-native safe construct.”
When To Use It
Reach for Singleton (the genuine single-instance invariant) when all of these hold:
- There is a true domain reason only one instance can exist (hardware, a pool, a process-wide registry), not merely convenience.
- The object is created once and lives for the whole process.
- Callers don’t need to vary or substitute the implementation — including in tests.
Even then, prefer the language-native form: a Meyers singleton in C++, a module-level object in Python, a single bean/provider in a DI container on the JVM/TS. These give you the single-instance guarantee without hand-rolling locks.
When NOT To Use It (the honest part)
Singleton is an anti-pattern far more often than it’s the right call. The objections:
1. It’s global mutable state in a costume. getInstance() is reachable from anywhere, so any method can mutate shared state with no signal in its signature. This is the exact problem we spent decades training out of programmers via “avoid globals.”
2. It hides dependencies. A function that calls Config.getInstance() has a dependency on Config, but its signature doesn’t say so. You can’t tell what a class needs by looking at its constructor — you have to read the whole body. This breaks local reasoning.
3. It wrecks testability. Because the dependency is hard-wired to a static accessor, you can’t inject a fake. Tests share one instance, so state leaks between tests, creating order-dependent flakiness. Resetting it requires ugly back-doors.
4. It lies about lifecycle and ordering. Eager statics create init-order fiascos (the C++ “static initialization order fiasco” across translation units). The Meyers singleton fixes construction ordering (it constructs lazily on first access), but two related caveats remain: accessing one Meyers singleton from another’s destructor at process exit, or from before main begins, can still touch an object that’s already destroyed or not yet constructed. So “Meyers solves init order” is true only for construction-on-first-use, not for cross-singleton teardown.
5. It tends to grow. Because it’s reachable everywhere, a singleton accretes responsibilities and becomes a god object.
“I made it a Singleton so I wouldn’t have to pass it around” is a smell, not a justification. The passing-around is the visible dependency wiring — the thing you actually want. Singleton doesn’t remove the coupling; it hides it.
What to prefer: dependency injection
The fix for “I need exactly one and everyone needs it” is almost always: create exactly one at the composition root and inject it.
// Instead of Service reaching for ConnectionPool.getInstance(),
// inject the pool. Service depends on an interface, not a global.
interface Pool {
acquire(): string;
release(c: string): void;
}
class OrderService {
// Dependency is explicit in the constructor signature.
constructor(private readonly pool: Pool) {}
placeOrder(): void {
const conn = this.pool.acquire();
try { /* ... use conn ... */ }
finally { this.pool.release(conn); }
}
}
// Composition root (main / DI container): the ONE place that decides
// there is one pool. Single-instance without global access.
// const pool = await ConnectionPool.getInstance();
// const service = new OrderService(pool);
//
// In tests you inject a fake — no global state, no leakage:
// const service = new OrderService(fakePool);DI keeps the single-instance guarantee (one object made at the root) while throwing away the global-access liability (callers receive it explicitly and can be handed a substitute).
Relationship to SOLID
| Principle | How Singleton interacts |
|---|---|
| SRP | Singleton conflates two responsibilities: managing its own lifecycle/uniqueness and doing its business work. SRP says these should be separate (a factory/container manages lifecycle; the class does the work). |
| OCP | The static getInstance hard-binds clients to a concrete type. You can’t extend/swap the implementation without editing call sites — closed to extension where you’d want it open. |
| LSP | Mostly N/A — there’s no subtype hierarchy, which is itself the problem: you can’t substitute a Liskov-compatible variant because callers reach a concrete static. |
| ISP | N/A directly, though god-object singletons accrete fat interfaces that force clients to depend on methods they don’t use. |
| DIP | The headline violation. Clients depend on a concrete class via a static call, not on an abstraction — the inverse of “depend on abstractions.” DI exists precisely to fix this. |
In an interview, if asked “is Singleton SOLID?”, the crisp answer is: “It tends to violate DIP and SRP. It couples callers to a concrete global instead of an injected abstraction, and it merges lifecycle management with business behavior. I’d keep the single-instance invariant but deliver it through DI — construct one at the composition root and inject an interface.”
Sibling Patterns It’s Confused With
| Pattern | What it actually does | Why it’s confused with Singleton |
|---|---|---|
| Monostate | Many instances, but all share static state, so they behave as one. | Same “acts like one” effect, opposite mechanism — instances are free to create, state is global. Hides global state even more sneakily. |
| Static utility class | All-static methods, no instance at all. | Looks like global access, but holds no per-object state and can’t implement an interface or be injected. Strictly less flexible. |
| Factory / Abstract Factory | Controls how objects are created (possibly many). | Both are creational; Factory governs creation policy and can be made to return one shared instance (so a Factory can deliver singleton semantics without the global accessor). |
| Multiton | A keyed registry of one-instance-per-key. | ”Singleton, but N of them” — same uniqueness invariant scoped per key. |
| Borg (Python) | Like Monostate: distinct instances sharing __dict__. | A Python idiom often offered as a “nicer Singleton”; it’s really Monostate. |
If an interviewer pushes “how is your Singleton different from a class of static methods?”, the load-bearing answer is identity and substitutability: a Singleton is an object, so it can implement an interface, be passed as a value, be mocked, and be replaced by DI. A static utility class can do none of those. That’s also exactly why “make it a DI-managed single instance behind an interface” is the upgrade.