Skip to content
aipatterns.dev

Design patternCreational Patterns

Singleton

Guarantee one instance of a class per process with a global access point. Usually better replaced by one instance created at startup and injected.

Complexity
Low
Seen in the wild
Medium

Intent

Ensure a class has exactly one instance in the process and give code a well-known way to reach it. The class itself controls its creation, so no caller can make a second copy by accident.

Forces

  • One shared resource vs many callers: a metrics registry, connection pool or cache must be shared, or its data splits across copies.
  • Global access vs visible dependencies: reaching for Registry.instance() anywhere is convenient, but hides what a function depends on.
  • Lazy creation vs thread safety: creating on first use is cheap until two threads race to do it.
  • One instance in production vs fresh instances in tests: state that leaks between tests makes them order-dependent.

Therefore

Create the one instance in one place, and hand it to the code that needs it. Use the class-enforced global accessor only at the edge, if at all.

The classic form hides the constructor and exposes a static accessor that creates the instance lazily and safely. That guarantees uniqueness, but it also makes the instance a global variable. The modern form keeps the "one instance" part and drops the "global access" part: the composition root creates the object once (or asks the accessor once) and passes it through constructors. Most languages also have a cheaper way to get one instance: a module-level value (TypeScript, Python), sync.OnceValue (Go), or a holder class or enum (Java).

Structure

Diagram source (Mermaid)
classDiagram
  class MetricsSink {
    <<interface>>
    +increment(name) void
  }
  class MetricsRegistry {
    -MetricsRegistry instance
    -Map counters
    +instance() MetricsRegistry
    +increment(name) void
    +snapshot() Map
  }
  class CheckoutService {
    -MetricsSink metrics
    +placeOrder(id) void
  }
  class CompositionRoot {
    +main() void
  }
  MetricsSink <|.. MetricsRegistry
  CheckoutService --> MetricsSink
  CompositionRoot ..> MetricsRegistry : gets the instance once
  CompositionRoot ..> CheckoutService : injects it

Participants

  • Singleton (MetricsRegistry): holds the single instance and controls its creation, lazily and thread-safely.
  • Abstraction (MetricsSink): the narrow interface consumers depend on, so tests can pass a fake.
  • Consumers (CheckoutService): receive the instance through their constructor and never call the static accessor.
  • Composition root (main): the only place that touches the global accessor or module-level instance.

Use it when

  • Exactly one instance must exist for correctness, not convenience: one process-wide metrics registry, one connection pool per database, one in-process cache whose entries must be shared.
  • The instance is expensive to create and must be created lazily, on first use.
  • A framework or plugin system offers no injection point, and the shared object has to be reachable from callbacks you don't construct.
  • You are wrapping a process-wide resource that is already unique: stdout, the environment, a native library handle.

Do not use it when

  • You want "only one" merely because the app creates only one. Create it in main and inject it. Uniqueness enforced by convention at the composition root is enough, and it keeps the dependency visible.
  • One instance per process is enough and load-time creation is acceptable. A module-level constant (export const metrics = new MetricsRegistry() in TypeScript or Python) is already a per-process single instance, with no private constructor or static accessor. Singleton's controlled construction matters only when creation is expensive, must wait for runtime configuration, or a second instance would be a bug.
  • You use a DI container. Register the class with singleton scope (Spring beans, .NET AddSingleton, NestJS providers) and let the container own its lifetime.
  • The object holds per-request or per-user state. A singleton turns that into cross-request leakage, and in multi-tenant code into a data exposure bug.
  • Tests need isolation. Every Foo.instance() call inside business logic is a hidden global that tests can't replace without reset hooks. Pass the instance as a parameter instead.
  • Configuration is the "shared" thing. Parse config once and pass the values; a global config object makes every function implicitly depend on every setting.

Application grill

Inspect first: the shared instances today (module scope in Node is already one per process; DI containers; global mutable state); how tests reset or fake the instance now; whether "one" means one per process or one across the fleet.

Missing pieces: none: module scope plus constructor injection replaces it. If "one" really means fleet-wide (one scheduler, one leader), that is a different problem - a database advisory lock or leader election - and this pattern does not provide it.

Ask before you build:

  • Scale: does "one" mean per process, per instance, or across the fleet?
  • Performance: does lazy initialization stall the first request that pays for setup?
  • Reliability: when the single instance holds state, what happens on redeploy or restart - who rebuilds it?
  • Budget: what does the global state cost in test isolation (order-dependent suites, flaky CI)?
  • Implementation: how is the instance provided - import side effect or injection - and how do tests substitute it?

Implementation steps

  1. Confirm a second instance would be a bug, not just a waste. If not, stop and inject a normal instance.
  2. Extract a narrow interface for what consumers actually call (increment), and change consumers to take it through their constructor.
  3. Create the instance in one place, preferring the language's idiom: a module-level value, sync.OnceValue, a holder class or an enum.
  4. If you need class-enforced uniqueness, hide the constructor and add a static accessor that creates the instance lazily and thread-safely.
  5. Call the accessor only at the composition root. Search for remaining instance() calls in business code and replace them with injected parameters.
  6. In tests, construct consumers with a fake or a fresh instance. Don't add resetForTests() hooks to the singleton.

Example

A web service counts business events (orders placed, payments failed) in one in-process metrics registry that a /metrics endpoint reads. Two registries would silently split the counts. Each version below uses its language's idiom for one instance, and CheckoutService receives it through its constructor.

interface MetricsSink {
  increment(name: string): void;
}

class MetricsRegistry implements MetricsSink {
  private static current: MetricsRegistry | undefined;
  private readonly counters = new Map<string, number>();

  private constructor() {}

  // Classic accessor. A module-level `export const metrics = ...` gives the same
  // guarantee in a module system; JavaScript is single-threaded, so no lock is needed.
  static instance(): MetricsRegistry {
    return (MetricsRegistry.current ??= new MetricsRegistry());
  }

  increment(name: string): void {
    this.counters.set(name, (this.counters.get(name) ?? 0) + 1);
  }

  snapshot(): Record<string, number> {
    return Object.fromEntries(this.counters);
  }
}

class CheckoutService {
  // Depends on the interface, so tests pass a fake instead of the global.
  constructor(private readonly metrics: MetricsSink) {}

  placeOrder(orderId: string): void {
    if (!orderId) {
      this.metrics.increment("orders.rejected");
      return;
    }
    this.metrics.increment("orders.placed");
  }
}

// Composition root: the only place that touches the accessor.
const registry = MetricsRegistry.instance();
const checkout = new CheckoutService(registry);
checkout.placeOrder("A-1001");
checkout.placeOrder("A-1002");
checkout.placeOrder("");

console.log(registry === MetricsRegistry.instance()); // true
console.log(registry.snapshot()); // { 'orders.placed': 2, 'orders.rejected': 1 }

// In a test: no global involved.
const seen: string[] = [];
new CheckoutService({ increment: (n) => seen.push(n) }).placeOrder("T-1");
console.log(seen); // [ 'orders.placed' ]

Trade-offs

Gains

  • A second instance can't be created by mistake.
  • Lazy, thread-safe creation is solved once, in one place.
  • Every part of the process sees the same shared state.

Costs

  • The accessor is a global variable: any function can depend on it without saying so in its signature.
  • Tests share state across cases unless every consumer takes the instance as a parameter.
  • It mixes two jobs, the object's real work and lifetime management, in one class.
  • "One per process" breaks down across worker processes, serverless instances or clustered nodes, where each gets its own copy.
  • Lazy initialization in languages with threads is easy to get wrong when written by hand.

Relations

  • Facade objects are often needed only once, but that is a reason to create one, not to make the class a singleton.
  • Flyweight also shares instances, but many of them, one per distinct intrinsic state. A singleton is exactly one, with mutable state allowed.
  • Abstract Factory implementations, builders and prototype registries are often made singletons. Usually a module-level value or a single injected instance is enough.
  • Builder and Prototype registries are similar candidates: one shared catalog, created at startup and passed around.

Agent checklist

  • A second instance would cause incorrect behavior, not only extra memory. Otherwise create one instance at startup and inject it.
  • You used the language idiom (module-level value, sync.OnceValue, holder class, enum, or DI singleton scope) rather than hand-rolled locking.
  • Business code receives the instance through a constructor or parameter; the static accessor is called only at the composition root.
  • The singleton holds no per-request, per-user or per-tenant state.
  • Tests construct consumers with a fake or fresh instance and need no reset hook on the singleton.

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.