Singleton Pattern — When It's Right and Wrong (2026)
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
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro