Singleton Pattern
The One-Instance Rule
LinkedIn Hook
Two config objects. Different values. Same app.
That is the exact moment a production bug becomes a four-hour debugging session. The Singleton pattern exists because some things — config, loggers, database pools, feature flags — must have exactly one source of truth, and "just be careful" is not a strategy.
In JavaScript you have four ways to enforce this: a closure-based factory, a class that returns a cached instance from its constructor, an
Object.freeze()shield that survives HMR throughSymbol.for()andglobalThis, and the most elegant option of all — the humble ES module.This lesson walks through all four, shows exactly when a Singleton saves you and when it secretly destroys your tests, and gives you the exact interview-ready answers for each.
Read the full lesson -> [link]
#JavaScript #SingletonPattern #DesignPatterns #InterviewPrep #CodingInterview #SoftwareArchitecture
What You'll Learn
- Four ways to implement a Singleton in JavaScript — closure, class, frozen, ES module
- How to make a singleton survive module reloads during hot module replacement
- The real-world cost of Singletons and when to deliberately avoid the pattern
One Config to Rule Them All
Imagine your application has a configuration manager. You wouldn't want two different config objects floating around with potentially different values — that would be chaos. The Singleton pattern ensures that a class or module produces exactly one instance, and provides a global access point to it.
Closure-Based Singleton
// Singleton using closure
const ConfigManager = (function () {
let instance = null;
function createInstance() {
// Private state
const config = {};
return {
set(key, value) {
config[key] = value;
},
get(key) {
return config[key];
},
getAll() {
return { ...config };
},
has(key) {
return key in config;
}
};
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// Always the same instance
const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();
config1.set("apiUrl", "https://api.example.com");
config2.get("apiUrl"); // "https://api.example.com"
console.log(config1 === config2); // true — same instance
Class-Based Singleton
class Database {
constructor(connectionString) {
if (Database._instance) {
return Database._instance;
}
this.connectionString = connectionString;
this.connected = false;
this.queryLog = [];
Database._instance = this;
}
connect() {
if (this.connected) return;
console.log(`Connecting to ${this.connectionString}...`);
this.connected = true;
}
query(sql) {
if (!this.connected) {
throw new Error("Not connected. Call connect() first.");
}
this.queryLog.push({ sql, timestamp: Date.now() });
console.log(`Executing: ${sql}`);
return { rows: [], sql };
}
disconnect() {
this.connected = false;
console.log("Disconnected.");
}
static getInstance(connectionString) {
if (!Database._instance) {
new Database(connectionString);
}
return Database._instance;
}
static resetInstance() {
Database._instance = null;
}
}
Database._instance = null;
// Usage
const db1 = Database.getInstance("postgres://localhost:5432/mydb");
const db2 = Database.getInstance("mysql://other-server"); // ignored — already created!
console.log(db1 === db2); // true
console.log(db2.connectionString); // "postgres://localhost:5432/mydb"
Frozen Singleton (Immutable)
// Singleton that cannot be mutated after creation
function createLogger() {
const logs = [];
const logger = {
log(message) {
const entry = {
message,
level: "INFO",
timestamp: new Date().toISOString()
};
logs.push(entry);
console.log(`[${entry.level}] ${entry.timestamp}: ${message}`);
},
error(message) {
const entry = {
message,
level: "ERROR",
timestamp: new Date().toISOString()
};
logs.push(entry);
console.error(`[${entry.level}] ${entry.timestamp}: ${message}`);
},
getHistory() {
return [...logs]; // return copy
},
clear() {
logs.length = 0;
}
};
// Freeze prevents adding/modifying/deleting properties
return Object.freeze(logger);
}
// Singleton that survives module reloads
const Logger = (() => {
// Use globalThis to persist across module reloads in dev environments
const SINGLETON_KEY = Symbol.for("app.logger.singleton");
if (!globalThis[SINGLETON_KEY]) {
globalThis[SINGLETON_KEY] = createLogger();
}
return globalThis[SINGLETON_KEY];
})();
// Cannot tamper with the singleton
Logger.newProp = "hack"; // silently fails (frozen)
delete Logger.log; // silently fails (frozen)
Logger.log("App started");
Logger.error("Something went wrong");
Logger.getHistory(); // [{ message: "App started", ... }, { message: "Something went wrong", ... }]
ES Module Singleton (The Modern Way)
// logger.js — ES modules are singletons by default
const logs = [];
export function log(message) {
logs.push({ message, level: "INFO", timestamp: Date.now() });
console.log(`[INFO] ${message}`);
}
export function error(message) {
logs.push({ message, level: "ERROR", timestamp: Date.now() });
console.error(`[ERROR] ${message}`);
}
export function getHistory() {
return [...logs];
}
// Any file that imports this module gets the SAME logs array
// because ES modules are evaluated once and cached
When to Use / When NOT to Use
Use Singleton for:
- Configuration managers
- Database connection pools
- Application-wide loggers
- Cache managers
- Feature flag services
Avoid Singleton when:
- Unit testing — global state makes tests interdependent and hard to isolate
- Tight coupling — every module that uses the singleton is coupled to it
- Concurrency — in server-side JS with worker threads, global state is dangerous
- When you actually need multiple instances — don't force a singleton just because you think there should be one
Common Mistakes
- Creating the "singleton" with
newin more than one place and being surprised when state diverges — the constructor trick (return Database._instance) must be in every entry point, or replace it with a singlegetInstance()factory. - Losing your singleton instance during hot module replacement because the module re-evaluates — use
Symbol.for()onglobalThisto anchor the instance outside the module cache. - Reaching for a Singleton when you actually need dependency injection — Singletons look convenient but make unit tests share state across runs, which leads to flaky tests.
Interview Questions
Q: What is the Singleton pattern? When would you use it?
Singleton ensures only one instance of a class/object exists and provides a global access point. Use it for shared resources like config managers, loggers, or database connection pools where multiple instances would cause inconsistency or waste resources.
Q: How do you make a singleton survive module reloads (e.g., HMR in development)?
Use
Symbol.for()withglobalThis.Symbol.for("key")always returns the same symbol across all scopes, and storing the instance onglobalThisensures it persists even when the module is re-evaluated during hot module replacement.
Q: What are the downsides of Singleton?
- Global state makes unit testing difficult since tests share state. 2) Tight coupling — any module using the singleton depends on it directly. 3) Hidden dependencies — it's not obvious from function signatures that a singleton is being used. 4) Thread safety issues in concurrent environments.
Q: Name 3 real-world use cases for Singleton.
Config managers (one source of truth for environment config), application-wide loggers (all modules write to the same sink), and database connection pools (reuse sockets, respect max-connection limits). Cache managers and feature-flag services are two more common ones.
Quick Reference — Cheat Sheet
SINGLETON — QUICK MAP
Goal:
Exactly ONE instance, global access point.
Implementations:
1. Closure -> IIFE + `let instance = null` + getInstance()
2. Class -> static _instance, constructor returns it
3. Frozen -> Object.freeze(instance) -> immutable API
4. ES Module -> singleton by default (evaluated + cached once)
Survive HMR / reloads:
const KEY = Symbol.for("app.logger.singleton");
globalThis[KEY] ??= createLogger();
export default globalThis[KEY];
Use for:
config, loggers, DB pools, caches, feature flags
Avoid because:
hides dependencies, breaks unit tests, shared state in workers
Previous: Module Pattern -> Private Scope via Closure Next: Observer / PubSub -> Subscribe, Don't Poll
This is Lesson 12.2 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.