GUIDE · Creational
Singleton
Ensure a class has exactly one instance and provide a global point of access to it.
At a glance
- 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);processobject; Redux single store; mongoose default connection; PrismaClient held at module scope; AngularprovidedIn: '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.
The idea#
Picture the pencil sharpener bolted to the classroom wall. Every kid walks to that one sharpener. The shavings pile up in one tray. If you bolted a second sharpener to the same wall, you would have two trays, two blades, and nobody would know which one was the real sharpener. You do not build a sharpener when you need a point on your pencil. You go to the one the room already has.
A singleton is that sharpener. The class is allowed one instance, and there is one way to ask for it. In the textbook shape that way is a static getInstance, and the constructor is not for outside code to call. The first request builds the object. Every later request returns that same object. Arguments on the later requests do not rebuild it and do not retune it. The first call wins.
In JavaScript you often get this without a class. A module runs its top level once per process. Export an object from that module, and every require of that file hands back the same object. The module cache is the bolt in the wall. A closure can do the lazy version: nothing is built until the first call, and the built count stays at one.
Why it matters#
The bug this pattern prevents is two of something the program can only survive one of. A connection pool, a logger, a config, a clock, a cache. Two pools means a split view of the database. Two caches means one part of the app stored a value the other part will never see. Sharing one instance means acquire through one handle shows up on the other, because they are the same object.
You already use this without the textbook name. Node's module cache is the usual one: module.exports = new Logger() runs once, and every require returns it. The process object is a singleton the runtime gives you. A Redux store is one store for the app. Mongoose's default connection and a Prisma client kept at module scope are the same idea. Angular's providedIn: 'root' services are one instance for the application.
A flyweight is the neighbor that sounds similar and is not. A flyweight is one instance per key, many keys, many instances. A singleton is exactly one. A static bag of functions, or a namespace object, can be just as global, and it has no instance and no lifecycle. You did not construct it, and you cannot tell when it started. A class singleton and a module singleton want the same thing. The module version is the everyday one, and it is easier to reset when a test needs a clean wall.
A small example#
function mountSharpener() { let only = null; return function getSharpener() { if (!only) only = { shavings: 0 }; return only; };} const getSharpener = mountSharpener();const morning = getSharpener();const afternoon = getSharpener();morning.shavings += 1;const oneSharpener = morning === afternoon && afternoon.shavings === 1;Morning and afternoon walked to the same wall. One tray of shavings. oneSharpener is true.
What the code below is doing#
The sheet shows the class shape, then the module shape you would actually ship, then a lazy closure, and it ends on why a bolted sharpener gets in the way of tests.
The first shape uses a class so the book name is visible. ConnectionPool is the singleton. getInstance is the access point. A private flag records the one instance. Another private flag, constructing, is the key in the case. The constructor throws "Use ConnectionPool.getInstance()" unless that flag is set. getInstance sets the flag, constructs once with the url, clears the flag, and stores the instance. A later getInstance('postgres://other/ignored') returns the original object, still on postgres://localhost/app. acquire bumps connections and returns an id plus the url. Calling acquire through one variable makes the other variable report 1, because they are the same object. new ConnectionPool('x') throws.
The second shape is how you would write it on a normal day. A tiny moduleCache map stands in for Node's require cache, so the file can stay self-contained. fakeRequire runs the factory only when the name is missing, then returns the cached object. Two requires of ./logger are the same logger. log('boot') through the first shows up in the second's lines. Under that, getClient is the lazy closure you use for an expensive client. The first call builds { id: 'redis' } and increments built. The second call returns the same client. built stays 1.
The note under the code is part of the lesson. A hard singleton is hidden global state. Tests cannot substitute it, state leaks between tests, and import order starts to matter. Prefer a factory you inject, or a reset hook. A module singleton clears when the module registry resets, or when that file's require.cache entry is deleted. "One" means one per process. Anything that must be unique across processes belongs in Redis or a database, not in one process's memory.
When you talk about it, say this: one instance, one access point. The singleton exposes getInstance and keeps construction to itself. In JavaScript the module cache already gives you that for free.
Everything below is the demonstration. Same idea, written as code you can run.
'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');Verified by 4 asserts
assert.equal(a, b); // same objectassert.equal(b.url, 'postgres://localhost/app'); // first call wins; later args ignoreda.acquire();assert.equal(b.connections, 1); // shared state visible through either handleassert.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'); // 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; };})(); // 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.Verified by 4 asserts
assert.equal(log1, log2);assert.deepEqual(log2.lines, ['boot']);assert.equal(getClient(), getClient());assert.equal(built, 1);See also