Skip to content

Singleton

Singleton ensures a class is instantiated exactly once and exposes that single instance through a shared access point, creating it lazily on first use.

Some things in a program are genuinely singular: the parsed configuration, a connection pool, an in-memory registry, a logger. If every part of the system constructs its own copy, they drift out of sync and waste resources. You want one object, created the first time anyone asks, and reused forever after — and you want that guarantee enforced rather than relying on convention.

The subtle part is who enforces it. Ordinary code can call a constructor as many times as it likes. Singleton moves construction behind a controlled accessor so the class itself owns the rule. The price is that the instance becomes global state, with all the testing and concurrency hazards that implies — which is why the discipline of one matters more than the trick of hiding the constructor.

classDiagram
  class Singleton {
    -instance: Singleton$
    -Singleton()
    +getInstance() Singleton$
    +operation()
  }
  Singleton --> Singleton : holds static reference
A Singleton holds a static reference to its own sole instance
  • Singleton — the class itself. It hides its constructor, holds a private static reference to the one instance, and exposes a static accessor that creates the instance on first call and returns the same reference thereafter.
  • Client — any caller. It never constructs the object directly; it asks the accessor for the shared instance.

A small read-only application config that loads once and is shared everywhere. Each language uses its idiomatic mechanism for “create exactly once, lazily, and safely.”

class AppConfig {
private static instance: AppConfig | null = null;
readonly env: string;
readonly maxConnections: number;
private constructor() {
// Pretend this reads from disk or the environment once.
this.env = process.env.APP_ENV ?? 'development';
this.maxConnections = 10;
}
static get(): AppConfig {
if (AppConfig.instance === null) {
AppConfig.instance = new AppConfig();
}
return AppConfig.instance;
}
}
const a = AppConfig.get();
const b = AppConfig.get();
console.log(a === b); // true — same shared instance
console.log(a.env, a.maxConnections);
  • Pro: guarantees a single instance and one obvious access point for genuinely shared state.
  • Pro: the instance is created lazily, so you pay the setup cost only if it is actually used.
  • Con: it is global mutable state in disguise; hidden dependencies make code harder to reason about.
  • Con: it complicates testing — tests share one instance and cannot easily inject a fake unless you design for it.
  • Con: naive lazy initialisation is a race condition under concurrency; you must guard it (as sync.Once and OnceLock do above).
  • Abstract Factory is often implemented as a Singleton so one factory is shared.
  • Prototype offers the opposite stance — make many copies cheaply rather than share one.
What does a Singleton guarantee?
Why does naive lazy initialisation need extra care under concurrency?
Which Go mechanism makes singleton construction run at most once?
What is the main testability drawback of Singletons?