Back to the path
Level 3 intermediate 25 min #design-patterns#creational#singleton#concurrency#dependency-injection#solid#lld

Singleton: One Instance, Global Access — and Why You Often Shouldn't

A rigorous treatment of the Singleton pattern: thread-safe implementations across TypeScript, Python, and C++ (eager, double-checked locking, holder idiom, module-level, Meyers singleton), the precise problems it solves, and an honest accounting of why it is frequently an anti-pattern — plus what to reach for instead (dependency injection).

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

Key idea

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:

  1. The constructor is private (or otherwise non-public), so no external code can new it.
  2. A static field holds the single instance.
  3. A static accessor (getInstance) returns that field, lazily creating it on first call (or eagerly at load time).

Participants

ParticipantRole
SingletonDeclares the static accessor, owns the static instance field, hides its constructor, and carries the actual business behavior.
ClientObtains 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:

StrategyLanguage homeHow it’s safeCost
Eager initAny (static field initialized at load)Created before any thread races; no lockBuilt even if never used; init-order issues across translation units
Double-checked locking (DCL)Java/C#/C++ (with care)First check lockless, second under lockSubtle: 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 holderJVM onlyNested static class loaded lazily by classloader; JVM guarantees safetyJVM-specific; no equivalent in C++/JS/Python, so it doesn’t appear in the tabs above
Meyers singletonC++11+Function-local static; compiler-inserted guardThe right default in C++; watch destruction order at process exit
Module-level objectPython (and ES modules)Module body runs once under the import lockThe 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().

Common pitfall

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.

Watch out

“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

PrincipleHow Singleton interacts
SRPSingleton 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).
OCPThe 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.
LSPMostly 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.
ISPN/A directly, though god-object singletons accrete fat interfaces that force clients to depend on methods they don’t use.
DIPThe 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

PatternWhat it actually doesWhy it’s confused with Singleton
MonostateMany 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 classAll-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 FactoryControls 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).
MultitonA 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.
Tip

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.

Assessment

Pass mark: 70% on the concept questions unlocks the next lesson.

1. An interviewer asks why Singleton is so often called an anti-pattern, given that the single-instance invariant is sometimes legitimately needed. Which answer best isolates the *actual* source of the trouble?

2. Which of the following statements about thread-safe Singleton construction are correct? (Select all that apply.) (select all)

3. A teammate replaces a Singleton `ConnectionPool` with a plain class, constructs one instance in `main()`, and passes it into every service's constructor as a `Pool` interface. Which benefit is the PRIMARY one this refactor buys, per the lesson's SOLID analysis?

4. Which statement most accurately characterizes the relationship between Singleton and the Monostate (a.k.a. Borg) pattern?

Design problem 5

You are building an in-process `MetricsRegistry` that must aggregate counters across an entire multi-threaded service (C++ or a no-GIL Python 3.13 build). The current code base exposes it as a classic Singleton: `MetricsRegistry::getInstance()` called from dozens of call sites, with an `increment(name)` method that does a check-then-insert into a map. Two problems are reported: (a) tests are flaky because counter state leaks between test cases, and (b) there is a rare lost-update under load. Redesign this. Describe the target structure, how you preserve the single-instance invariant without the testability cost, and how you make `increment` thread-safe. Provide a short code sketch (any one language) for the core type and the composition-root wiring.