Limit Instantiation With Singleton

Destination
Symptom

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.

Goal

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 }
Example source: Illustrative example written for this site, faithful to Kerievsky's pattern shape in Refactoring to Patterns (Addison-Wesley, 2004), chapter 5. The book is careful that Singleton is for cases where instance uniqueness is part of the contract — this metrics-registry example fits that frame: every contributor must report into one place or the snapshot lies.
Pressure

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

Tradeoff

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.

Relief

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.

Trap

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.