Singleton Pattern
One Instance to Rule Them All
LinkedIn Hook
Here's an OOP interview question that trips up senior developers:
"What is the Singleton pattern — and when should you NOT use it?"
Most candidates nail the first half. They explain one instance, global access, the classic implementation. Then the interviewer asks the follow-up: "What are the downsides?" Silence.
Knowing a pattern is not enough. You have to know when it hurts you.
In this lesson, you'll learn how Singleton works under the hood in JavaScript, how Node.js module caching gives you Singleton behavior for free, and the specific situations where reaching for Singleton will make your codebase harder to test and maintain.
Read the full lesson → [link]
#OOP #JavaScript #NodeJS #DesignPatterns #InterviewPrep
What You'll Learn
- What the Singleton pattern is and the real problem it solves
- How to implement lazy initialization in JavaScript
- How Node.js module caching makes Singleton nearly automatic
- The thread-safety concern and why it matters conceptually for Node.js
- Practical use cases: config, logger, and database connection
- When NOT to use Singleton, and why interviewers ask about this specifically
The Analogy That Makes Singleton Click
Think about the government of a country. There is exactly one. You don't create a second government when a new ministry needs funding. Every ministry, every department, and every public service routes its requests through the same single institution. The government has a global point of access, and there's an enforcement mechanism preventing a duplicate from being created.
That's Singleton. One instance. One global access point. A built-in guard that prevents duplication.
Now think about what would happen if each department secretly created its own parallel government. Tax records would be inconsistent. Laws would conflict. No two departments would share the same state. That's exactly the bug Singleton prevents when you have shared resources like a configuration object or a database connection pool.
What is the Singleton Pattern?
The Singleton pattern is a creational design pattern that restricts a class to a single instance and provides a global access point to that instance. Every caller that asks for the object gets the exact same object back, not a copy, not a clone, the same reference.
It solves two problems in one:
- Instance control - guarantees that only one instance ever exists
- Global access - provides a consistent way to reach that instance from anywhere in the application
The pattern was formally described in the Gang of Four book "Design Patterns: Elements of Reusable Object-Oriented Software" (1994), which remains the canonical reference for the 23 classic patterns.
Example 1 — Classic Singleton with Lazy Initialization
Lazy initialization means the instance is not created when the class is defined. It's created the first time it's actually requested. This avoids wasting resources on an object that might never be needed.
class AppConfig {
// The single instance is stored here, starts as null
static #instance = null;
// Private constructor prevents direct instantiation
#settings;
constructor() {
// Guard: if someone calls new directly, throw
if (AppConfig.#instance) {
throw new Error("Use AppConfig.getInstance() instead of new");
}
// First-time initialization of settings
this.#settings = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
};
console.log("AppConfig initialized (this should print only once)");
}
// The controlled access point
static getInstance() {
if (!AppConfig.#instance) {
AppConfig.#instance = new AppConfig(); // Lazy: created on first call
}
return AppConfig.#instance;
}
get(key) {
return this.#settings[key];
}
set(key, value) {
this.#settings[key] = value;
}
}
// First call: creates the instance
const config1 = AppConfig.getInstance();
console.log(config1.get("apiUrl")); // Output: https://api.example.com
// Second call: returns the same instance (no new initialization)
const config2 = AppConfig.getInstance();
config2.set("timeout", 10000);
// config1 and config2 are the same object
console.log(config1.get("timeout")); // Output: 10000 (same reference)
console.log(config1 === config2); // Output: true
Two things make this a proper Singleton. First, the private #instance field stores the single reference. Second, getInstance() checks whether the instance already exists before creating a new one. The constructor guard is a defensive backstop.
[INTERNAL-LINK: private class fields → Lesson 2.3: Private Fields in JavaScript]
Example 2 — Logger Singleton
A logger is one of the clearest real-world uses for Singleton. Every part of your app writes to the same log stream. You need consistent formatting and you need all messages to flow through the same handler.
class Logger {
static #instance = null;
#logs = [];
constructor() {
if (Logger.#instance) {
return Logger.#instance; // Silently return existing instance
}
Logger.#instance = this;
}
static getInstance() {
if (!Logger.#instance) {
new Logger();
}
return Logger.#instance;
}
log(level, message) {
const entry = {
timestamp: new Date().toISOString(),
level,
message,
};
this.#logs.push(entry);
console.log(`[${entry.timestamp}] [${level.toUpperCase()}] ${message}`);
}
getHistory() {
return [...this.#logs]; // Return a copy, not the internal array
}
}
// Different modules requesting the logger — all get the same instance
const loggerA = Logger.getInstance();
const loggerB = Logger.getInstance();
loggerA.log("info", "Server started on port 3000");
loggerB.log("warn", "Memory usage above 80%");
// Both loggers share the same history
console.log(loggerA.getHistory().length); // Output: 2
console.log(loggerA === loggerB); // Output: true
Notice getHistory() returns a spread copy of the internal array. This protects the internal state from mutation by callers. That's encapsulation working alongside the Singleton pattern.
How Node.js Module Caching Gives You Singleton for Free
Node.js caches modules after the first require() or import. Every subsequent call to the same module returns the cached exports. That means a module that exports a single object instance is already a Singleton. You don't need the static #instance dance.
// db-connection.js — module-based Singleton in Node.js
class DatabaseConnection {
constructor(host, port) {
this.host = host;
this.port = port;
this.isConnected = false;
console.log(`DB connection object created: ${host}:${port}`);
}
connect() {
if (this.isConnected) {
console.log("Already connected, reusing existing connection");
return;
}
// Simulate connecting
this.isConnected = true;
console.log(`Connected to ${this.host}:${this.port}`);
}
query(sql) {
if (!this.isConnected) {
throw new Error("Not connected to database");
}
console.log(`Executing query: ${sql}`);
// Return mock data
return [];
}
}
// Create the instance once at module level
const db = new DatabaseConnection("localhost", 5432);
// Export the single instance, not the class
module.exports = db;
// user-service.js
const db = require("./db-connection");
db.connect();
db.query("SELECT * FROM users");
// Output: Connected to localhost:5432
// Output: Executing query: SELECT * FROM users
// order-service.js
const db = require("./db-connection");
// Node.js returns the cached module — same db object
db.connect();
// Output: Already connected, reusing existing connection
db.query("SELECT * FROM orders");
// Output: Executing query: SELECT * FROM orders
The constructor message "DB connection object created" prints exactly once, no matter how many files require the module. Node.js handles the Singleton guarantee through its module system. This is the approach most production Node.js applications use.
Thread Safety — What It Means and Why Node.js Mostly Avoids It
Thread safety is a concern in multi-threaded languages like Java. If two threads both call getInstance() at the exact same moment, both may find #instance === null simultaneously, and both may create a new instance. That breaks the Singleton guarantee.
JavaScript is single-threaded. The event loop processes one thing at a time. In a typical Node.js application, you won't hit the classic race condition from multi-threaded languages. However, two edge cases are worth knowing for interviews.
First, if you use worker_threads in Node.js, each worker runs its own JavaScript context with its own module cache. Each worker creates its own instance. The Singleton boundary is per-process, not per-worker.
Second, if your getInstance() path involves async operations (reading config from a file, for example), you can have a race condition even in Node.js if two async callers both hit the null check before either resolves. The fix is to store the Promise itself, not the resolved value.
class AsyncConfig {
static #instancePromise = null;
static getInstance() {
// Store the Promise on first call, return same Promise on subsequent calls
if (!AsyncConfig.#instancePromise) {
AsyncConfig.#instancePromise = AsyncConfig.#initialize();
}
return AsyncConfig.#instancePromise;
}
static async #initialize() {
// Simulate async config load (e.g., reading a file or env service)
await new Promise(resolve => setTimeout(resolve, 50));
return { apiUrl: "https://api.example.com", timeout: 5000 };
}
}
// Multiple callers — all await the same Promise
async function main() {
const [config1, config2] = await Promise.all([
AsyncConfig.getInstance(),
AsyncConfig.getInstance(),
]);
console.log(config1 === config2); // Output: true (same resolved object)
}
main();
Storing the Promise is the key insight. The null check happens synchronously before either async operation completes, so both callers receive the same Promise and both resolve to the same value.
[PERSONAL EXPERIENCE]: In production Node.js services, the async Singleton pattern is the most common place this trips up teams. The constructor-based lazy init works fine for synchronous setup. The moment config loading goes async, teams often introduce subtle duplication bugs by not caching the Promise.
When NOT to Use Singleton
This section is what separates candidates who understand patterns from those who only memorize them. Interviewers specifically ask about Singleton's downsides because overuse of Singleton is a well-known code smell.
Problem 1 — It Makes Testing Hard
Singletons carry state between tests. If test A modifies the Singleton and test B runs immediately after, test B starts with corrupted state. You can't isolate tests cleanly.
// Hard to test — the Singleton persists across test runs
const config = AppConfig.getInstance();
config.set("apiUrl", "https://mock-api.test"); // Mutates the global state
// Later test expects the original value — it breaks
console.log(AppConfig.getInstance().get("apiUrl")); // "https://mock-api.test" - polluted
The standard fix is dependency injection. Pass the config object as a parameter rather than reaching for the global instance inside functions. That way, tests can pass a mock config object without touching the Singleton.
[UNIQUE INSIGHT]: The real problem with Singleton is not the pattern itself but the direct access pattern it encourages. AppConfig.getInstance() called deep inside a function creates a hidden dependency. Hidden dependencies are the enemy of testability. If you must use Singleton, inject it through constructors so the dependency is visible.
Problem 2 — It Introduces Hidden Global State
Any part of the application can mutate the Singleton. As the codebase grows, tracking what modified the Singleton and when becomes difficult. This is the same complaint lodged against global variables, because a mutable Singleton effectively is one.
Problem 3 — It Violates the Single Responsibility Principle
A Singleton class is responsible for both its own business logic and for managing its own instantiation. That's two responsibilities. Better practice is to manage the single-instance constraint at the module or dependency injection container level, keeping the class itself clean.
When Singleton is Appropriate
Use Singleton for genuinely shared, stateless-or-immutable resources:
- Read-only configuration loaded once at startup
- Logger instances (though even here, consider a logging library that handles this internally)
- Connection pools (the pool object itself, not individual connections)
- Registry objects that map keys to values and never change after initialization
Avoid Singleton when the object holds mutable shared state that different callers read and write independently, or when you need to swap implementations in tests.
Common Mistakes
1. Returning undefined from getInstance() on first call
Forgetting to return this or the instance in the constructor when using the constructor-return trick causes silent failures. Always use a separate static method pattern (static getInstance()) to keep the logic explicit.
2. Not protecting the constructor
Without the constructor guard, new AppConfig() bypasses getInstance() entirely and creates a second instance. Always throw an error or silently return the existing instance from the constructor.
3. Assuming Node.js module Singleton works across workers
Each worker_thread has its own module cache. A module-based Singleton is scoped to one process. Don't assume shared state between workers.
4. Making the Singleton mutable when it should be immutable
A config Singleton that any module can write to is a global variable with extra steps. Freeze the settings after initialization, or expose only read methods.
// Freeze after initialization to prevent mutation
const settings = Object.freeze({
apiUrl: "https://api.example.com",
timeout: 5000,
});
5. Using Singleton to avoid thinking about architecture
Singleton is sometimes used as a shortcut to share data between disconnected parts of an application when the real fix is to restructure the data flow. If you find yourself adding more and more things to a Singleton, treat that as a design warning sign.
Interview Questions
Q1: What is the Singleton pattern and what problem does it solve?
The Singleton pattern ensures a class has exactly one instance and provides a global access point to it. It solves the problem of shared resources (config, logger, DB connection) where multiple instances would cause inconsistency, wasted resources, or conflicting state.
Q2: How does lazy initialization differ from eager initialization in Singleton?
Eager initialization creates the instance when the class loads, regardless of whether it's needed. Lazy initialization defers creation until getInstance() is first called. Lazy is preferred when the object is expensive to create or might not be needed in every execution path.
Q3: Why does Node.js module caching give you Singleton behavior?
Node.js caches modules after the first require(). Every subsequent require() for the same module returns the cached exports without re-executing the module file. If the module exports a single instance, all callers share that exact same object reference.
Q4: What are the main downsides of the Singleton pattern?
Three main downsides. First, it makes unit testing hard because the instance persists across tests, carrying state from one test into the next. Second, it introduces global mutable state, making it difficult to trace which part of the application changed the Singleton. Third, it creates hidden dependencies - code that calls getInstance() internally has a dependency that's not visible in the function signature, which violates dependency inversion principles.
Q5: How would you make a Singleton testable?
Inject the Singleton through constructors or function parameters instead of calling getInstance() directly inside functions. This makes the dependency explicit. In tests, pass a mock object with the same interface. For cases where injection isn't practical, add a resetInstance() method (visible only in test environments) that sets #instance = null so each test starts fresh.
Cheat Sheet
SINGLETON PATTERN — QUICK REFERENCE
Purpose:
Ensure one instance exists. Provide global access to it.
Core structure:
- static #instance = null (stores the single reference)
- private constructor (prevents direct new calls)
- static getInstance() (lazy init + access point)
Lazy init check:
if (!Class.#instance) {
Class.#instance = new Class();
}
return Class.#instance;
Node.js shortcut:
const instance = new MyClass();
module.exports = instance; // Node module cache = Singleton
Async Singleton:
Cache the Promise, not the resolved value.
static #promise = null;
static getInstance() {
if (!Class.#promise) Class.#promise = Class.#init();
return Class.#promise;
}
Good use cases:
- Read-only config
- Logger
- DB connection pool
- Registry / cache
Bad use cases:
- Mutable shared state
- Objects that need to be swapped in tests
- Replacing proper dependency injection
Testability fix:
Inject the Singleton — don't reach for getInstance() inside functions.
Thread safety in Node.js:
- Single-threaded event loop: classic race condition does not apply
- Worker threads: each worker has its own module cache — no sharing
- Async code: cache the Promise to prevent double-initialization
Navigation
- Previous: Lesson 8.1 - What Are Design Patterns?
- Next: Lesson 8.3 - Factory Pattern
This is Lesson 8.2 of the OOP Interview Prep Course — 8 chapters, 41 lessons.