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.
Multiple instances of a class that should be unique appear at construction sites the agent must trace to confirm state coherence. State-fragmentation bugs are invisible to local reasoning — each instance looks correct, but their sum is wrong.
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.
One static accessor the agent verifies once; all references resolve to the same identity. The agent can statically reason about 'same instance' rather than tracking which constructor produced which instance.
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).
Multiple-instance fragmentation cannot be statically diagnosed — the agent needs runtime introspection or holistic test coverage to detect when two callers hold different instances of what should be one. Verification cost is high; bugs are subtle.
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.
Singletons defeat the agent's static dependency analysis: constructor signatures no longer reveal what a class uses. Test isolation requires the agent to inject a reset hook or restructure to allow per-test instances; tests can become order-sensitive without obvious indicators.
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.
Identity is statically guaranteed; the agent can verify that all callers refer to the same conceptual instance without runtime introspection. Cross-cutting invariants (one cache, one registry) become enforceable at construction.
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.
Singletons hide dependencies from the agent's static view; the agent must grep for static accessors to enumerate consumers. Test-order flakiness from leaked state is invisible until it manifests; the agent cannot statically detect Singletons that should have been per-instance.