A class with shared mutable state is instantiated multiple times across the codebase. Each instance accumulates its own state; consumers of the state see partial views because they hold different instances; the conceptual 'one thing' is split into accidental siblings.
Construction is funnelled through a single static accessor that returns the same instance every time. The class's contract enforces uniqueness; the conceptual 'one thing' has one runtime identity.
Before the refactoring
// Anyone can construct a MetricsRegistry; each instance has its own state.// Counts reported to one registry are invisible to another.class MetricsRegistry {constructor() {this.counters = new Map();}increment(name) {this.counters.set(name, (this.counters.get(name) ?? 0) + 1);}snapshot() {return Object.fromEntries(this.counters);}}const registryA = new MetricsRegistry();registryA.increment('orders.created');const registryB = new MetricsRegistry();registryB.snapshot(); // {} — does not see registryA's increment.
After the refactoring
// One instance for the whole process; getInstance() is the only way to obtain it.class MetricsRegistry {static #instance = null;static getInstance() {if (!MetricsRegistry.#instance) {MetricsRegistry.#instance = new MetricsRegistry(MetricsRegistry.#construction);}return MetricsRegistry.#instance;}static #construction = Symbol('private construction key');constructor(key) {if (key !== MetricsRegistry.#construction) {throw new Error('MetricsRegistry is a singleton; use getInstance()');}this.counters = new Map();}increment(name) {this.counters.set(name, (this.counters.get(name) ?? 0) + 1);}snapshot() {return Object.fromEntries(this.counters);}}MetricsRegistry.getInstance().increment('orders.created');MetricsRegistry.getInstance().snapshot(); // { 'orders.created': 1 }
Multiple instances of what should be one cause silent state-fragmentation bugs — metrics that don't add up, caches that don't share, registries that disagree. The symptom (a snapshot showing partial data) is far from the cause (instantiation discipline).
Singletons couple consumers to a global accessor; they leak state across tests unless explicitly reset; they hide dependencies from constructor signatures. A Singleton applied to a class that doesn't need uniqueness is the Inline Singleton candidate — friction without payoff.
One conceptual thing has one runtime identity; consumers stop accidentally fragmenting state; reasoning about 'is this the same registry?' becomes trivial. Construction-time invariants run once.
Singletons applied for convenience rather than for uniqueness contract become global-state vectors that resist testing and refactoring. The pattern is right when uniqueness is part of the class's contract; wrong when it's a shortcut for skipping dependency injection.