JavaScript Interview Prep
Design Patterns

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 through Symbol.for() and globalThis, 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


Singleton Pattern thumbnail


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

Singleton Pattern visual 1


Common Mistakes

  • Creating the "singleton" with new in 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 single getInstance() factory.
  • Losing your singleton instance during hot module replacement because the module re-evaluates — use Symbol.for() on globalThis to 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() with globalThis. Symbol.for("key") always returns the same symbol across all scopes, and storing the instance on globalThis ensures it persists even when the module is re-evaluated during hot module replacement.

Q: What are the downsides of Singleton?

  1. 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.

On this page