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-testedgetInstance() 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 state

Java 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 state

Anti-Pattern Callout: Why getInstance() Breaks Unit Tests

Three reasons why the getInstance() pattern makes classes untestable:

  1. 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.
  2. Global mutable state — tests contaminate each other. The static instance field persists across test runs in the same process. A test that mutates the singleton leaks state to all subsequent tests, causing order-dependent failures.
  3. Locks the concrete type — cannot substitute a test double. Production code that calls getInstance() is hardwired to ConfigService. There is no seam to inject a FakeConfigService or StubConfigService without 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.

ConceptRelationship
Factory-Method-PatternAlternative creational pattern; ConcreteCreator can be Singleton-scoped via DI
Abstract-Factory-PatternConcreteFactory objects are often Singleton-scoped; use DI rather than getInstance()
Builder-PatternDirector is sometimes Singleton-scoped (use DI instead)
Prototype-PatternSingleton prevents multiple instances; Prototype enables independent copies — opposing forces
  • 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)