Singleton pattern
Singleton pattern is a creational design pattern that restricts a class to a single instance and provides a global access point to it.
Theory
TL;DR
- Think of it like the White House: one building, one president, everyone goes through the same address.
- Main difference: a regular class lets you call
newas many times as you want; Singleton blocks everynewafter the first one. - Use when exactly one object should control a shared resource (logger, DB pool, config). Skip it for stateless utilities.
- In Node.js clusters, each process gets its own Singleton instance. It is NOT shared across processes.
Quick example
class Singleton {
private static instance: Singleton;
private constructor() {
// private: blocks new Singleton() from outside
}
static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton(); // created once
}
return Singleton.instance;
}
}
const a = Singleton.getInstance();
const b = Singleton.getInstance();
console.log(a === b); // true - same objectTwo calls, one object. That is the whole mechanism.
Key difference
A plain class lets anyone call new and get a fresh object each time. Singleton makes the constructor private and routes all access through a static getInstance() method. The first call creates the object and stores it in a static field. Every call after that returns the same stored reference. No new allocations, no duplicates.
When to use
- Logger - one log stream for the whole app, not one per module.
- DB connection pool - share limited connections across the codebase.
- App config - load settings once at startup, read anywhere.
- Cache - one in-memory store, consistent state across features.
- Skip it when the class holds no shared state. A utility with pure functions does not need Singleton. If you need to mock it in tests, consider dependency injection instead.
How the engine handles this
V8 (Node.js/Chrome) stores the private static field #instance as a hidden class property, inaccessible from outside. Each call to getInstance() does a fast static field lookup. If the field is null, it allocates once via new and caches the reference. Subsequent calls skip allocation entirely.
One thing that catches developers off guard: in Node.js with worker threads or in cluster mode, each process or worker has its own memory space. So each gets its own Singleton instance. This trips up most developers the first time they deploy to a load-balanced environment. If you actually need shared state across processes, use an external store like Redis, not a Singleton.
Common mistakes
Clusters break the "one instance" guarantee
// Each worker process prints a different PID
const singleton = require('./singleton');
console.log(singleton.getInstance().id); // worker 1: 1234, worker 2: 1235If your app runs multiple Node.js processes, Singleton does not give you a single shared instance. Use IPC or Redis for that.
Public constructor lets anyone bypass the gatekeeper
// Wrong
class Config {
constructor() { this.settings = {}; } // nothing stops new Config()
static getInstance() { /* ... */ }
}
const c1 = new Config(); // bypasses getInstance entirely
const c2 = Config.getInstance(); // different objectMake the constructor private (TypeScript) or throw inside it (plain JS).
Testing becomes painful with shared state
// Test pollution - Logger keeps state between tests
test('logs error', () => {
Logger.getInstance().log('error');
});
test('empty log on start', () => {
// Logger still has the entry from the previous test
expect(Logger.getInstance().getLogs()).toHaveLength(0); // fails
});Add a resetInstance() method for test environments or use dependency injection instead of a bare Singleton.
Serialization crashes on circular reference
JSON.stringify(Singleton.getInstance()); // Error: circular structureThe static instance field points back to the object itself. Add a toJSON() method that returns only the data you need.
Real-world usage
- Winston (Node logger):
createLogger()returns one logger per transport config. - Express: the
appobject is one instance per server process. - Redux DevTools: one store instance hooks into the React app.
- React Context: a Provider creates one value shared down the component tree, which behaves as a Singleton for that subtree.
- Lodash: the utility object is a module-level Singleton, though you should import individual functions for tree-shaking.
When you genuinely need testable, mockable instances, look at InversifyJS or plain dependency injection. Singleton is for true uniques.
Follow-up questions
Q: How do you make a Singleton thread-safe in JavaScript?
A: In single-threaded JS, a simple if (!instance) check is fine. With worker threads, concurrent getInstance() calls during async initialization can race. Use ??= for atomic-ish assignment or guard the init with a flag.
Q: What is the difference between Singleton and a global variable?
A: A global variable is just a value, anyone can overwrite it. Singleton controls creation (lazy init, exactly once) and can carry methods, validation, and state. Both share the problem of hidden dependencies.
Q: How does Singleton violate SOLID?
A: It breaks Single Responsibility (the class manages its own lifecycle AND does its actual job) and Dependency Inversion (callers hardcode getInstance() instead of receiving an interface).
Q: How do you mock a Singleton in Jest?
A: Use jest.spyOn(MyClass, 'getInstance').mockReturnValue(mockInstance). Or add a static resetInstance() that clears the private field and call it in beforeEach.
Q: (Senior) Why avoid Singleton for a DB pool in microservices?
A: Each pod scales independently and needs its own pool sized to its load. A Singleton inside one process is fine, but the assumption of "one pool for the whole app" breaks when the app is split across 20 pods. Use a connection string in config and a pool library like pg-pool per service.
Examples
Basic: TypeScript Singleton for app config
class AppConfig {
private static instance: AppConfig;
private settings: Record<string, string> = {};
private constructor() {
// Config is loaded exactly once
this.settings = { env: process.env.NODE_ENV ?? 'development' };
}
static getInstance(): AppConfig {
if (!AppConfig.instance) {
AppConfig.instance = new AppConfig();
}
return AppConfig.instance;
}
get(key: string): string {
return this.settings[key] ?? '';
}
}
const config1 = AppConfig.getInstance();
const config2 = AppConfig.getInstance();
console.log(config1 === config2); // true
console.log(config1.get('env')); // 'development'Both variables hold the same object. settings is loaded exactly once, no matter how many modules call getInstance().
Intermediate: Shared logger in Express
class Logger {
static #instance;
#logs = [];
constructor() {
if (Logger.#instance) throw new Error('Use Logger.getInstance()');
Logger.#instance = this;
}
static getInstance() {
return Logger.#instance ?? (Logger.#instance = new Logger());
}
log(message) {
const entry = `${new Date().toISOString()}: ${message}`;
this.#logs.push(entry);
console.log(entry);
}
getLogs() {
return this.#logs;
}
}
// Both routes share the same Logger and the same #logs array
app.get('/users', (req, res) => {
Logger.getInstance().log('GET /users');
res.json([]);
});
app.post('/orders', (req, res) => {
Logger.getInstance().log('POST /orders');
res.json({ id: 1 });
});Both routes call getInstance() and get back the same object, so #logs accumulates entries from every request in one place.
Senior: Why tests break without instance reset
class Counter {
static #instance;
#count = 0;
static getInstance() {
return Counter.#instance ?? (Counter.#instance = new Counter());
}
static resetInstance() {
Counter.#instance = null; // expose only for tests
}
increment() { this.#count++; }
value() { return this.#count; }
}
// Test file
beforeEach(() => Counter.resetInstance());
test('starts at zero', () => {
expect(Counter.getInstance().value()).toBe(0); // passes every time
});
test('increments correctly', () => {
Counter.getInstance().increment();
expect(Counter.getInstance().value()).toBe(1); // passes every time
});Without resetInstance(), the second test would see a count of 1 left over from the first. The shared instance is exactly why Singletons pollute test state when you do not plan for it.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.