Singleton Pattern
The singleton pattern restricts a “class” to a single instance and gives the rest of the program a global point of access to it. You reach for it when exactly one object should coordinate something shared — a configuration store, a connection pool, a cache, a logger. In JavaScript the pattern is unusually natural because the module system already gives you one-instance-per-module semantics for free, but it is also one of the most over-used patterns, so knowing when not to use it matters as much as knowing how.
The core idea
A singleton guarantees two things: only one instance is ever created, and everyone who asks for it gets that same instance. The classic way to enforce this is to cache the instance the first time it is requested and hand back the cached copy on every subsequent request.
class Logger {
constructor() {
if (Logger.instance) {
return Logger.instance;
}
this.entries = [];
Logger.instance = this;
}
log(message) {
this.entries.push(message);
console.log(`[log] ${message}`);
}
}
const a = new Logger();
const b = new Logger();
a.log("first");
b.log("second");
console.log(a === b);
console.log(a.entries.length);
Output:
[log] first
[log] second
true
2
Because the constructor returns the cached Logger.instance whenever it already exists, new Logger() always yields the same object. Both a and b point at one shared entries array.
The module-level instance
In modern JavaScript the simplest and most idiomatic singleton is just a module that exports an object. ES modules are evaluated once and the resulting bindings are cached, so every import of the module receives the same exported value.
// config.js
let settings = { theme: "dark", retries: 3 };
export function get(key) {
return settings[key];
}
export function set(key, value) {
settings = { ...settings, [key]: value };
}
// app.js
import * as config from "./config.js";
config.set("theme", "light");
console.log(config.get("theme"));
Output:
light
No class, no caching logic, no risk of accidentally constructing a second copy — the module is the single instance. This is the form you should prefer in greenfield code.
Tip: A bare exported object is a singleton too, but exporting functions (as above) keeps the internal state truly private and prevents callers from reassigning your shared object.
Lazy initialization
Sometimes the instance is expensive to build, so you want to defer creation until the first real use. A factory function with a cached variable gives you lazy, on-demand instantiation while still preserving privacy through closure.
function createConnectionPool() {
let instance;
return function getPool() {
if (!instance) {
console.log("creating pool...");
instance = { connections: [], createdAt: Date.now() };
}
return instance;
};
}
const getPool = createConnectionPool();
const p1 = getPool();
const p2 = getPool();
console.log(p1 === p2);
Output:
creating pool...
true
Notice “creating pool…” prints only once — the second call reuses the closed-over instance.
Implementation options compared
| Approach | Lazy? | Private state | Best for |
|---|---|---|---|
| Module-level export | No (eager) | Yes | Most app-level singletons |
| Closure factory | Yes | Yes | Expensive/deferred resources |
Class with cached instance | Optional | No (static is reachable) | OOP-heavy codebases, familiarity |
| Frozen object literal | No | No | Simple constant config |
When it becomes an anti-pattern
The singleton’s strength — global access to shared mutable state — is also its biggest liability. Because any module can read and mutate the instance, a singleton creates hidden coupling that is hard to trace and hard to test. Tests that share a singleton can leak state into one another, producing order-dependent failures.
Warning: A singleton holding mutable state is effectively a global variable wearing a tie. If two unrelated parts of your app both mutate it, you have reintroduced exactly the problems modules were meant to solve.
Prefer passing dependencies explicitly (dependency injection) when you need testability, or when “one instance” is really an assumption that might change later. Singletons are safe and pleasant for genuinely process-wide, mostly-immutable concerns; they turn brittle the moment “shared” turns into “shared and freely mutated everywhere.”
Best Practices
- Reach for a plain ES module export before writing any explicit singleton machinery — it is the idiomatic JS singleton.
- Keep the shared state private; expose getters/setters rather than the raw object so callers cannot reassign it.
- Use a closure factory when initialization is expensive and should be lazy.
- Make the instance immutable (
Object.freeze) when it represents constants — this removes the worst class of singleton bugs. - Avoid singletons for anything you need to swap or reset in tests; inject the dependency instead.
- Provide a reset/teardown hook if a singleton must hold mutable state in a test suite.
- Document that the value is a singleton so future maintainers do not create rogue second instances.