// ───────────────────────────────────────────────────────────────────────────── // SINGLETON [Creational] // ───────────────────────────────────────────────────────────────────────────── // INTENT : Ensure a class has exactly one instance and provide a global point of access to it. // PROBLEM : Many parts of the app need to share ONE thing (config, connection pool, logger) and // constructing it twice is wasteful or outright wrong (two pools, two caches, two clocks). // SAY : Participants: Singleton (static getInstance(), non-public constructor). Pitch: "one instance, // one access point — in JS the module cache already gives you this for free." // SEEN IN : Node module cache (`module.exports = new Logger()` runs once per process); `process` object; // Redux single store; mongoose default connection; PrismaClient held at module scope; // Angular `providedIn: 'root'` services. // VS : Flyweight = ONE instance PER KEY (many keys); Singleton = exactly one. Static class / namespace // object = same global reach but no instance/lifecycle. Module singleton vs class singleton: // same intent, module version is idiomatic and easier to reset in tests. // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ── Classic shape (classes, so the participant names are visible) class ConnectionPool { static #instance = null; static #constructing = false; constructor(url) { if (!ConnectionPool.#constructing) throw new Error('Use ConnectionPool.getInstance()'); this.url = url; this.connections = 0; } static getInstance(url = 'postgres://localhost/app') { if (!ConnectionPool.#instance) { ConnectionPool.#constructing = true; ConnectionPool.#instance = new ConnectionPool(url); ConnectionPool.#constructing = false; } return ConnectionPool.#instance; } acquire() { this.connections += 1; return { id: this.connections, url: this.url }; } } const a = ConnectionPool.getInstance(); const b = ConnectionPool.getInstance('postgres://other/ignored'); assert.equal(a, b); // same object assert.equal(b.url, 'postgres://localhost/app'); // first call wins; later args ignored a.acquire(); assert.equal(b.connections, 1); // shared state visible through either handle assert.throws(() => new ConnectionPool('x'), /getInstance/); // ── Idiomatic JS shape: module-scope export (the require() cache IS the singleton registry) // In a real file: // // logger.js // const logger = { lines: [], log(m) { this.lines.push(m); } }; // module.exports = logger; // every `require('./logger')` returns this same object // Simulated with a tiny fake module registry so this file stays self-contained. const moduleCache = new Map(); const fakeRequire = (name, factory) => { if (!moduleCache.has(name)) moduleCache.set(name, factory()); return moduleCache.get(name); }; const loggerModule = () => ({ lines: [], log(m) { this.lines.push(m); } }); const log1 = fakeRequire('./logger', loggerModule); const log2 = fakeRequire('./logger', loggerModule); log1.log('boot'); assert.equal(log1, log2); assert.deepEqual(log2.lines, ['boot']); // Lazy variant via closure — the usual shape for expensive clients (Prisma, Redis, SDK clients) let built = 0; const getClient = (() => { let client; return () => { if (!client) { built += 1; client = { id: 'redis', built }; } return client; }; })(); assert.equal(getClient(), getClient()); assert.equal(built, 1); // Testability note: a hard singleton is hidden global state. Tests can't substitute it, state leaks // between tests, and import order starts to matter. Prefer: export a factory and inject the instance // (DI), or expose a reset hook for tests. Module singletons reset with jest.resetModules() / // `delete require.cache[require.resolve('./logger')]`. Serverless/multi-process: "one per process", // never "one per system" — anything that must be truly unique belongs in Redis/DB, not memory. console.log('ok: design-patterns/creational/singleton.js');