January 8, 2026 (2mo ago)

The singleton design pattern: Pros, Cons, and Modern Alternatives

Learn the singleton design pattern, its drawbacks, and modern alternatives like DI for cleaner, testable TypeScript code.

← Back to blog
Cover Image for The singleton design pattern: Pros, Cons, and Modern Alternatives

Learn the singleton design pattern, its drawbacks, and modern alternatives like DI for cleaner, testable TypeScript code.

Singleton Pattern: Drawbacks & Modern Alternatives

Summary: Understand the singleton pattern, why it often causes testability and scaling problems, and modern alternatives like dependency injection and module patterns for TypeScript.

Introduction

The singleton pattern ensures a class has only one instance and provides a global access point to it. That convenience can be tempting, but in modern TypeScript and Node.js projects it often creates hidden coupling, brittle tests, and scaling headaches. This article explains what singletons do, why they’re controversial, and practical, testable alternatives you can adopt today.

What the Singleton Pattern Actually Solves

A detailed line drawing of an air traffic control tower surrounded by twelve various airplanes.

Imagine an air traffic control tower coordinating dozens of planes. The tower represents a single authority that prevents collisions and confusion. Similarly, a singleton guarantees one shared object for a cross-cutting concern—configuration, logging, or a connection pool—so every part of the app uses the same centralized resource.

The pattern enforces two things:

  • Ensure a single instance by hiding the constructor.
  • Provide global access through a static accessor like getInstance().

That combination gives you global convenience at the cost of introducing global state, which has long-term consequences for maintainability and testability1.

Why Developers Criticize Singletons

The singleton pattern often behaves like a global variable with a formal wrapper. That leads to three major problems:

  1. Hidden dependencies: Code that calls Singleton.getInstance() has an invisible dependency, making components harder to reason about and reuse.
  2. Poor testability: Shared state persists across tests and makes unit tests flaky and hard to isolate.
  3. Architectural coupling: Singletons break principles like the Single Responsibility Principle and Dependency Inversion, which leads to brittle designs that resist change.

These critiques are well established in the engineering community and across design literature1.

Implementing a Singleton in TypeScript (Classic)

A diagram illustrating the Singleton design pattern, showing a class with a private constructor and a static getInstance() method leading to a singleton object.

Below is the canonical implementation so you can recognize it in real code.

class SettingsManager {
  private static instance: SettingsManager;

  private constructor() {
    console.log("SettingsManager instance created.");
  }

  public static getInstance(): SettingsManager {
    if (!SettingsManager.instance) {
      SettingsManager.instance = new SettingsManager();
    }
    return SettingsManager.instance;
  }

  public getSetting(key: string): string {
    return `Value for ${key}`;
  }
}

const config1 = SettingsManager.getInstance();
const config2 = SettingsManager.getInstance();
console.log(config1 === config2); // true

Thread Safety Note

In multi-threaded languages you must guard against race conditions during initialization. Node.js runs on a single-threaded event loop, so JavaScript singletons don’t face the same concurrency risks at creation time, but thread-safety is a concern in other runtimes2.

Why Singletons Become an Anti-Pattern

At scale, singletons introduce global state and hidden coupling across codebases. When a class reaches out for a global instance, that dependency isn’t visible in the constructor or public API. You can’t identify the component’s needs by inspecting its interface, which makes reasoning and refactoring much harder.

Testability Problems

Because a singleton instance persists, tests that rely on it can leak state between runs. That makes unit tests brittle and non-deterministic. Modern test strategies favour dependency injection so test doubles can be passed in easily, keeping tests isolated and repeatable.

Violations of SOLID Principles

Singletons mix lifecycle management with business logic, violating the Single Responsibility Principle. They also force code to depend on concrete implementations rather than abstractions, working against the Dependency Inversion Principle and making components harder to substitute or mock.

Better Alternatives

Diagram illustrating Dependency Injection as a better alternative to the Singleton design pattern.

Here are practical, modern alternatives you should consider.

Dependency Injection (DI)

Instead of a class fetching a global instance, DI supplies dependencies from the outside. Constructor injection is the simplest form: the dependent class declares what it needs, and the composition root wires the concrete instances together. This makes dependencies explicit and easy to mock for tests4.

Refactoring the earlier example into a plain, instantiable class:

export class SettingsManager {
  constructor() {
    console.log("SettingsManager instance created.");
  }

  public getSetting(key: string): string {
    return `Value for ${key}`;
  }
}

A consumer receives it via constructor injection:

import { SettingsManager } from './SettingsManager';

export class UserProfile {
  constructor(private settings: SettingsManager) {}

  public loadProfileTheme(): string {
    const theme = this.settings.getSetting('theme');
    return `Theme set to: ${theme}`;
  }
}

A DI container like InversifyJS or a framework such as NestJS can manage the composition for you, or you can wire instances manually at the app entry point5.

ES Modules

Node and modern bundlers evaluate modules once and cache the result. Exporting an instantiated object from a module gives you a lightweight shared instance without singleton boilerplate. This is often suitable for simple shared state or utilities3.

Factory Pattern

If creation logic is complex or you need multiple configured instances, a factory centralizes creation without enforcing a single global instance. Factories give you control over lifecycles while keeping responsibilities separate.

Quick Comparison

PatternTestabilityFlexibilityBest for
Dependency InjectionExcellentHighLarge apps that need loose coupling and testability
ES ModulesGoodMediumSimple shared state in JS/TS apps
Factory PatternGoodMediumComplex creation logic or multiple configuration variants

Refactoring Roadmap: Replacing Singletons Safely

Refactoring singletons in a legacy codebase should be incremental and well tested.

  1. Identify every call to getInstance() and map the dependencies.
  2. Convert the singleton class to a normal class by making the constructor public while keeping the static accessor temporarily.
  3. Introduce an injection strategy at the composition root and start creating the dependency there.
  4. Replace consumers one by one to accept the dependency via constructor injection, running tests after each change.
  5. When no callers remain, remove the static accessor and instance.

This gradual, test-driven approach minimizes risk and keeps the application stable during migration. Adopting these practices can materially improve developer velocity and system resilience6.

Singletons in Distributed Systems: A Warning

The idea of a single instance breaks down in distributed systems. Each process will create its own singleton, which defeats the pattern. Attempts to enforce a global, cross-process singleton introduce bottlenecks and single points of failure and are an anti-pattern in distributed architectures7.

If you need coordination across services, prefer distributed coordination primitives (databases, message queues, consensus services) designed for that purpose rather than forcing a single-instance model across processes.

FAQs — Quick Answers

Q: When is a singleton acceptable?

A: Rarely. A singleton might be OK for a truly stateless wrapper around a single physical device, but most use cases (loggers, config) are better served with DI or module exports1.

Q: Doesn’t a singleton just replace globals cleanly?

A: It seems that way, but it still creates global state and hidden dependencies. The wrapper doesn’t solve the core problems of coupling and testability1.

Q: How do I start migrating a singleton in a large codebase?

A: Map all usages, make the constructor public, introduce a top-level composition root, and incrementally replace callers to receive dependencies via constructors. Keep the static API until every caller is migrated, then remove it.

Three Concise Q&A Sections (User-focused)

Q1: How does a singleton hurt unit testing?

A1: Because the same instance is shared across tests, one test can alter global state and cause other tests to fail unpredictably. DI lets you pass fresh test doubles into each test, keeping them isolated.

Q2: What is the simplest alternative for small TypeScript projects?

A2: Use ES module exports for shared utilities or a single instance. It’s simple, requires no extra framework, and still avoids explicit global accessor patterns3.

Q3: What should I use when building scalable microservices?

A3: Avoid distributed singletons. Prefer stateless services, message-driven coordination, or purpose-built distributed systems (datastores, queues) to coordinate state across services7.


At Clean Code Guy, we help teams replace fragile legacy patterns with maintainable architecture. If your codebase is tangled in globals and singletons, a careful refactor can restore testability and developer confidence.

1.
Martin Fowler, “Singleton,” Martin Fowler’s Bliki, https://martinfowler.com/bliki/Singleton.html
2.
Node.js Documentation, “The Node.js Event Loop,” https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
3.
Node.js Documentation, “Modules: Loading from Cache,” https://nodejs.org/api/modules.html#modules_caching
4.
“Dependency injection,” Wikipedia, https://en.wikipedia.org/wiki/Dependency_injection
5.
InversifyJS, Documentation, https://inversify.io/
6.
Nicole Forsgren, Jez Humble, and Gene Kim, “State of DevOps Report 2019,” Accelerate / DORA, https://services.google.com/fh/files/misc/state-of-devops-2019.pdf
7.
The Twelve-Factor App, “Twelve-Factor App methodology,” https://12factor.net/
← Back to blog
🙋🏻‍♂️

AI writes code.
You make it last.

In the age of AI acceleration, clean code isn’t just good practice — it’s the difference between systems that scale and codebases that collapse under their own weight.