Singleton Pattern
Singleton Pattern
Ensure a class has only one instance, and provide a global point of access to it — but prefer DI-managed lifecycle over getInstance().
Intent
The Singleton pattern restricts a class to exactly one instance and provides a global access point. The canonical implementation uses a private constructor and a static getInstance() accessor. In modern DI-centric architectures, the DI container manages the singleton lifecycle — this is the preferred approach.
When NOT to Use
Most Singleton usage in practice falls into one of these categories — reach for DI instead:
- Configuration data — inject via DI container instead; tests can inject a different config without global state
- Service objects (loggers, caches, repositories) — DI container manages singleton scope without exposing a static accessor
- Any context where the using class must be unit-tested —
getInstance()prevents substituting a test double (see Anti-Pattern Callout below) - When the "single instance" constraint is a convenience assumption, not a genuine system requirement — use DI scope instead; the framework enforces the constraint without coupling production code to a static method
When to Use (narrow)
- A shared resource that genuinely must have exactly one instance system-wide (e.g., a hardware device driver, a connection pool manager)
- The instance's identity matters — all consumers must be the same object (e.g., an in-process event bus)
- Even in these cases, prefer DI-managed singleton scope over
getInstance()
How It Works
Private constructor prevents direct instantiation. Static field holds the single instance. Static getInstance() creates on first call (lazy initialization) and returns on subsequent calls.
Class Diagram
classDiagram
class Singleton {
-instance$ : Singleton
-Singleton()
+getInstance()$ Singleton
+operation()
}
note for Singleton "- Constructor is private\n- instance is static, created on first call\n- getInstance() is the sole access point"
TypeScript Example
// AVOID: getInstance() breaks unit tests
class ConfigService {
private static instance: ConfigService;
private constructor(private config: Record<string, string>) {}
static getInstance(): ConfigService {
if (!ConfigService.instance) {
ConfigService.instance = new ConfigService({ env: 'prod' });
}
return ConfigService.instance;
}
get(key: string) { return this.config[key]; }
}// PREFER: module-level singleton — injectable as a constructor dependency
export class ConfigService {
constructor(private config: Record<string, string>) {}
get(key: string) { return this.config[key]; }
}
// Single instance created at module level — import and inject via constructor
export const configService = new ConfigService({ env: 'prod' });
// Tests: pass new ConfigService({ env: 'test' }) directly — no global stateJava Example
// PREFER: enum singleton — thread-safe and serialisation-safe (Bloch, Effective Java Item 3)
public enum ConfigService {
INSTANCE;
private final Map<String, String> config = new HashMap<>();
public String get(String key) { return config.get(key); }
}
// Usage: ConfigService.INSTANCE.get("env")// PREFER in Spring/Jakarta EE: let the DI container manage the singleton scope
// @Service
// public class ConfigService {
// public String get(String key) { ... }
// }
// Constructor injection: public class Consumer { public Consumer(ConfigService cs) {...} }
// Tests inject a different ConfigService instance — no getInstance(), no global stateAnti-Pattern Callout: Why getInstance() Breaks Unit Tests
Three reasons why the
getInstance()pattern makes classes untestable:
- Static call — cannot be mocked without bytecode manipulation. Standard mocking frameworks (Jest, Mockito) mock object references.
ConfigService.getInstance()is resolved at compile time — the mock never intercepts it without PowerMock/ts-jest static mocking hacks.- Global mutable state — tests contaminate each other. The static
instancefield persists across test runs in the same process. A test that mutates the singleton leaks state to all subsequent tests, causing order-dependent failures.- Locks the concrete type — cannot substitute a test double. Production code that calls
getInstance()is hardwired toConfigService. There is no seam to inject aFakeConfigServiceorStubConfigServicewithout modifying production code.DI Alternative: Declare the dependency via constructor parameter; let the caller or DI container provide the instance — tests pass a different instance, production code gets the single instance.
Related Concepts
| Concept | Relationship |
|---|---|
| Factory-Method-Pattern | Alternative creational pattern; ConcreteCreator can be Singleton-scoped via DI |
| Abstract-Factory-Pattern | ConcreteFactory objects are often Singleton-scoped; use DI rather than getInstance() |
| Builder-Pattern | Director is sometimes Singleton-scoped (use DI instead) |
| Prototype-Pattern | Singleton prevents multiple instances; Prototype enables independent copies — opposing forces |
Related System Design
- Unique-ID-Generator-Design — the ticket server (centralised counter for ID generation) is a globally unique instance; inherits Singleton's testing pitfall (hard to parallelise) and availability pitfall (single point of failure) — the DI alternative is directly applicable
Sources
- Gamma et al., Design Patterns, 1994, pp. 127–134 — authoritative definition
- Bloch, Effective Java 3rd ed., Item 3 — enum singleton and serialisation safety; Item 83 — lazy initialization
- Martin Fowler — "Singleton considered anti-pattern in DI contexts" (martinfowler.com/articles/injection.html)