Singleton
The Singleton pattern restricts a class to one instance globally. Use for shared resources (config, logger, DB pool); avoid when testability and concurrency matter more.
Implementations + pitfalls + alternatives
EXAMPLE
// 1) Module-as-singleton (the JavaScript way)
// db.js
let pool = null;
export function getPool() {
if (!pool) pool = new Pool({ /* config */ });
return pool;
}
export function closePool() {
pool?.end();
pool = null;
}
// Or just
// const pool = new Pool({ /* config */ });
// export default pool;
// Modules are cached by Node — importing twice returns the same instance.
// 2) Classic singleton class
class Logger {
static #instance = null;
static getInstance() {
if (!Logger.#instance) {
Logger.#instance = new Logger();
}
return Logger.#instance;
}
constructor() {
if (Logger.#instance) throw new Error('use Logger.getInstance()');
// setup
}
info(msg) { console.log('[info]', msg); }
}
Logger.getInstance().info('hello');
// 3) Lazy + thread-safe (Java, Python — note: GIL changes things in CPython)
// Java — double-checked locking with volatile
class Config {
private static volatile Config instance;
private Config() {}
public static Config getInstance() {
if (instance == null) {
synchronized (Config.class) {
if (instance == null) instance = new Config();
}
}
return instance;
}
}
// Python — module-level (idiomatic)
# config.py
# _instance = None
# def get_config():
# global _instance
# if _instance is None:
# _instance = Config()
# return _instance
// 4) Enum singleton (Java's safest)
enum Logger {
INSTANCE;
public void info(String msg) { ... }
}
// Logger.INSTANCE.info("hi")
// Serialisation-safe + reflection-safe + lazy.
// 5) Typical real uses
// - DB connection pool
// - Logger
// - App config
// - Feature-flag client
// - Service locator / registry (in absence of DI)
// - Hardware access (audio context, GPU device)
// 6) Why singletons get a bad rap
// - Implicit dependencies — hard to see who depends on what
// - Hard to test — global state leaks between tests
// - Hard to swap (e.g. mock in tests, switch impl in dev/prod)
// - Don't compose well with multi-tenant or multi-config scenarios
// - Concurrency bugs in eager initialisation
// - Tight coupling — your code knows about the global, not a contract
// 7) Better alternative: Dependency Injection
// Define a contract; let composition root choose the instance.
//
// // logger.ts
// export interface Logger { info(msg: string): void; }
// export class ConsoleLogger implements Logger { info(msg: string) { console.log(msg); } }
//
// // service.ts
// export class UserService {
// constructor(private logger: Logger) {}
// create(u: User) { this.logger.info(`created ${u.id}`); }
// }
//
// // bootstrap.ts
// const logger = new ConsoleLogger();
// const svc = new UserService(logger);
//
// // test.ts
// const fake = { info: vi.fn() };
// const svc = new UserService(fake);
//
// Modern DI frameworks: Angular, NestJS, Spring, .NET DI, Dagger, Inversify.
// 8) When a singleton is the right call
// - Truly one instance globally (process-wide cache, system clock)
// - Lifecycle matches process lifecycle
// - You don't need to mock it in tests (e.g. it has no behavior to mock)
// - You can't avoid it (third-party library requirement)
// 9) Make a singleton testable
// a. Expose a reset/init function for tests
// export function resetForTesting() { pool = null; }
// b. Inject a factory or pre-built instance — let production use the singleton, tests pass a mock
// c. Use a service container — the 'singleton' is registered, not hard-coded
// 10) Concurrency pitfalls
// - Eager init in a class loader can dead-lock with other class initialisers
// - Lazy init without synchronisation can create two instances under race
// - Don't put expensive blocking work in the constructor
// 11) Memory + lifecycle
// Singletons live for the lifetime of the process. Be careful with:
// - Listeners registered to the singleton — keep references; garbage never collected
// - Caches that grow unbounded — set max sizes or TTL
// - Resources (file handles, connections) — provide a clean shutdown method
// 12) Singleton vs Service Locator vs DI
// Singleton : caller asks the class itself (Class.getInstance())
// Service Locator: caller asks a registry (Container.get('logger'))
// DI : the caller is GIVEN its dependencies, doesn't ask
//
// DI > Service Locator > Singleton on testability + clarity.
// 13) Anti-pattern signs
// • You sprinkle Logger.getInstance() across 200 files
// • Tests fail because some other test left the singleton in a weird state
// • You wish there were two instances (dev + prod, two tenants) but the singleton blocks you
// • Adding a constructor argument requires touching every caller
// 14) Modern alternative — module + factory
// connection.ts
let client = null;
export function init(config) {
if (client) throw new Error('already initialised');
client = createClient(config);
return client;
}
export function getClient() {
if (!client) throw new Error('call init() first');
return client;
}
export function close() {
client?.close();
client = null;
}
// Forces explicit init at app start; safer than implicit lazy creation.
// 15) When debating 'should this be a singleton?'
// - Is there exactly one of this thing per process?
// - Will tests ever need a different impl?
// - Will multiple modules need it?
// If yes-no-yes → DI it.
// If yes-no-no → module-level instance.
// If yes-yes-yes → DI it; the singleton-ness is enforced at the composition root.
Why it matters
Singletons feel handy but punish tests + concurrency. Reach for module-level instances + dependency injection — you get the “one instance” benefit without the implicit-global headaches.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
class Config {
static #instance;
static get() { return this.#instance ??= new Config(); }
}
// Use sparingly — singletons make tests harder.
Try it Yourself »
Exercise
Singletons hide constructors and expose…
Config.
()
Three letters.
Discussion
Loading…