Every module that needs a piece of shared state constructs its own copy. The construction is expensive (disk read, network handshake, parser warm-up) and the copies drift the moment any consumer mutates them — but there is no architectural anchor saying which copy is canonical.
Exactly one instance exists for the system's lifetime; every consumer reaches that instance through a single named access point. Expensive setup runs once; the shared state has a canonical home; cross-consumer visibility is intentional, not accidental.
Before the pattern
class Config {constructor() {this.values = loadFromDisk();}}// every consumer constructs its own:const databaseModule = { config: new Config() };const httpModule = { config: new Config() };const loggerModule = { config: new Config() };// three disk reads; three copies that drift if the file changes;// no canonical 'current configuration' anywhere in the system.
After the pattern
class Config {static instance = null;static getInstance() {if (Config.instance === null) {Config.instance = new Config(Config.#TOKEN);}return Config.instance;}static #TOKEN = Symbol('Config.constructor');constructor(token) {if (token !== Config.#TOKEN) {throw new Error('Use Config.getInstance() — Config is a singleton');}this.values = loadFromDisk();}}const databaseModule = { config: Config.getInstance() };const httpModule = { config: Config.getInstance() };const loggerModule = { config: Config.getInstance() };
Per-consumer construction is Duplicated Code at the worst level — the work itself is duplicated, not just the source text. Cache invalidation, hot-reload, and disk re-read semantics multiply by consumer count. Drift bugs are catastrophic and slow to diagnose: which Config does this module trust?
Singletons are Global Data wearing a class costume. They couple every consumer to one concrete implementation; they break unit-test isolation because tests now share mutable state; they hide dependencies behind a static access point that linters can't trace. The pattern is famously over-used; every team has scars.
One canonical instance; one place to invalidate; one place to log access. Expensive setup runs once. Architectural intent is explicit: 'this is a system-wide shared resource.'
Singletons multiply: a justified 'just one more' becomes a fleet of twelve. Each new Singleton couples every consumer to it; testing accumulates per-Singleton reset boilerplate; modularity erodes faster than the convenience wins. Stop and reach for explicit dependency injection unless the resource is genuinely process-global (loaded config, system clock, randomness source) and the alternative is materially worse.