Lesson 7 / 25

Singleton and Why It Is Often an Anti-Pattern

One instance, at a cost.

Intent and problems

Intent: ensure a class has only one instance and provide a global access point to it, typically through a private constructor and a static getInstance(). Legitimate needs for a single shared instance exist: a connection pool, a configuration object, a logger. The problems come from the global access: any code can reach the instance, so dependencies are hidden, tests share state and are hard to isolate, and swapping the implementation for a fake is awkward. Singletons also complicate concurrency and lifecycle (when is it created and cleaned up?). Prefer creating one instance at the application's entry point (the composition root) and passing it to whoever needs it. In JavaScript and Python, a module-level instance is already a de facto singleton because modules are evaluated once and cached, so the classic class machinery is rarely needed.

Who owns the instance?

Deciding how many instances exist and who creates them shapes testability.

Three ideas: Singleton and its problems, Prototype and cloning, Dependency Injection.
Figure 3.1 — Single instances, copies and injected dependencies.

Classic singleton versus a module instance

Same outcome, less ceremony; injection is better still.

// Classic GoF singleton
class Config {
  private static instance: Config | undefined;
  private constructor(readonly values: Record<string, string>) {}
  static getInstance(): Config {
    if (!Config.instance) Config.instance = new Config(loadFromEnv());
    return Config.instance;
  }
}

// Module-level instance: modules are evaluated once and cached
// db.ts
export const db = createPool({ connectionString: process.env.DATABASE_URL });

// Better for testing: create once at startup, pass it in
class OrderRepository {
  constructor(private pool: Pool) {}
}
const pool = createPool({ connectionString: process.env.DATABASE_URL });
const orders = new OrderRepository(pool);

Single instance is not the same as Singleton

Having one instance of something is often correct. The anti-pattern is the global access point that lets any code grab it without declaring the dependency.

Quick check: Why is Singleton often considered an anti-pattern?

  • Global access hides dependencies and makes tests share state
  • It always uses too much memory
  • It cannot be used in TypeScript
  • It creates a new object on every call
Answer

Global access hides dependencies and makes tests share state — The global access point is the main problem.