Skip to content

Singleton Pattern — When It's Right and Wrong (2026)

DodaTech Updated 2026-06-20 5 min read

In this tutorial, you'll learn how the singleton pattern ensures a class has only one instance, why developers often misuse it, and when a singleton is genuinely the right tool for the job.

A country has one president. There's no scenario where two presidents should exist simultaneously — it would create confusion, conflicting orders, and chaos. The same applies to certain software components: a configuration manager, a logging service, a thread pool. You don't want multiple instances fighting over the same resource. The singleton pattern enforces that only one instance ever exists.

Core Concept

The singleton pattern restricts a class to a single instance and provides a global point of access to it. It combines two ideas: controlling instantiation (no external new calls) and providing a way to access the one instance from anywhere.

Thread-Safe Implementations

Eager Loading (Simplest and Safest)

class ConfigManager {
  private static readonly instance = new ConfigManager();
  private settings: Map<string, string> = new Map();

  // Private constructor — no external instantiation
  private constructor() {
    this.loadDefaults();
  }

  static getInstance(): ConfigManager {
    return ConfigManager.instance;
  }

  get(key: string): string {
    return this.settings.get(key) ?? "";
  }

  private loadDefaults() {
    this.settings.set("appName", "DodaTech");
    this.settings.set("version", "2.0");
  }
}

// Usage
const config = ConfigManager.getInstance();
console.log(config.get("appName")); // Output: DodaTech

Expected output: The instance is created when the class is loaded (safe, simple). The constructor is private — no one can create a second instance.

Lazy Loading with Double-Checked Locking

class Logger {
  private static instance: Logger | null = null;
  private static lock = new Object();

  private constructor() {}

  static getInstance(): Logger {
    if (!Logger.instance) {
      synchronized(Logger.lock, () => {
        if (!Logger.instance) {
          Logger.instance = new Logger();
        }
      });
    }
    return Logger.instance;
  }

  info(message: string): void {
    console.log(`[INFO] ${message}`);
  }
}

The double-check prevents two threads from both creating an instance. The first if avoids unnecessary locking after initialization. The inner if ensures only one thread creates the instance.

The Problem with Singletons

While singletons seem convenient, they cause real issues:

Hidden Dependencies

A class that calls Logger.getInstance() has a hidden dependency on the Logger class. Testing requires global state management. If tests don't clean up, one test's logger state leaks into another.

Global State

Singletons ARE global state. Any part of the application can access and modify them. This makes code unpredictable — changing a singleton's state in one place affects behavior everywhere.

Tight Coupling

Code that uses singletons is coupled to the singleton class itself. You can't swap implementations without changing every call site. This violates the Dependency Inversion Principle.

DI Container Alternative

Instead of a singleton class, register the instance as singleton-scoped in a Dependency Injection container:

// No singleton pattern — just DI with singleton lifetime
class Logger {
  info(message: string): void { /* ... */ }
}

class ConfigManager {
  get(key: string): string { /* ... */ }
}

// Container setup
container.register("Logger", {
  useClass: Logger,
  lifetime: "singleton"  // DI container manages the single instance
});

container.register("Config", {
  useClass: ConfigManager,
  lifetime: "singleton"
});

// Both services receive the same Logger instance
class UserService {
  constructor(
    private logger: Logger,      // injected by container
    private config: ConfigManager // injected by container
  ) {}
}

class OrderService {
  constructor(private logger: Logger) {} // same instance as UserService
}

Benefits: No hidden dependencies (they're in the constructor). Easy to swap for testing (pass a mock). The DI container manages lifetime — the class itself doesn't know it's a singleton.

Real-World Example

Durga Antivirus Signature Cache

Durga Antivirus Pro uses a signature cache shared across all scanning threads. Rather than a singleton class, the cache is registered as singleton in the DI container. The scan engine and update service both receive the same cache instance through constructor injection. Testing? No need to reset global state — the test creates a fresh container with a new cache instance.

When Singletons Are Actually Right

  • Logging — but only when using a DI container with singleton scope
  • Hardware interfaces — one printer, one GPU, one network interface
  • Configuration — but prefer loading into DI-scoped singletons
  • Caches — shared state that genuinely needs to be global
  • Thread pools — managing a fixed set of worker threads

If you must use a singleton directly (not through DI), ensure it's stateless or the state is truly application-global and doesn't need to vary per test or environment.

Pros & Cons

Pros Cons
Guarantees single instance Introduces global state
Lazy initialization possible Hidden dependencies
Simple to implement Difficult to unit test
Avoids duplicate resource usage Violates Single Responsibility (manages creation AND its purpose)

FAQ

When should I NOT use a singleton?

Avoid singletons for: database connections (connection pools are better), user sessions (there are many users), caching that needs to be invalidated per test, service classes that could have multiple implementations, and any class that should be mockable in tests.

Is a singleton the same as a static class?

No. A static class (all static methods) can't implement interfaces, can't be passed as a parameter, and can't participate in dependency injection. A singleton is a real instance — it can implement interfaces, be injected, and be swapped for testing.

How do I unit test code that uses a singleton?

The best approach is to refactor to DI. If you can't, the singleton should expose a setter or reset method for tests. Clean up in test setup/teardown to prevent test pollution. This is fragile — refactoring is safer long-term.

What about serialization and reflection breaking singletons?

Serialization can create multiple instances via deserialization. Override readResolve() to return the existing instance. Reflection can call private constructors. Defend by throwing in the constructor if the instance already exists, or use an enum singleton (Java).

Does the Factory Pattern conflict with singletons?

No — a Factory Pattern can return a singleton instance. The factory decides whether to create a new object or return a cached one. The client doesn't know or care. This is a common combination when object creation needs conditional logic but the result should be shared

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro