Singleton
Guarantees that a class has exactly one instance throughout the entire lifetime of the application and provides a global access point to that instance.
Intent
Ensure that a class is instantiated exactly once and provide a global access point to that instance, eliminating the need to pass the reference throughout the entire application.
Singleton is one of the 23 patterns from the original catalog by Gamma, Helm, Johnson and Vlissides — the book Design Patterns: Elements of Reusable Object-Oriented Software (1994), affectionately known as the "Gang of Four" (GoF). It belongs to the creational patterns category, which deal with object instantiation mechanisms.
Problem
Some responsibilities in a system need to exist in a single place: the database connection pool, the central logger, the in-memory cache, the settings loaded from a file, the event bus. Creating multiple instances of these structures causes inconsistencies: each instance keeps its own state, logs get fragmented, the cache gets duplicated.
The naive solution is to use a global variable. The problem: global variables let any part of the code overwrite the value, don't guarantee that initialization happens exactly once, and make dependencies implicit, making testing and maintenance harder.
Singleton solves this by encapsulating the instance-control logic inside the class itself, making it impossible (or at least explicitly difficult) to accidentally create a second instance.
Solution
The canonical Singleton implementation has three mandatory elements:
-
Private (or protected) constructor: prevents external code
from instantiating the class with
new. -
Private static field: stores the class's single instance
(the field starts as
nullorundefined). -
Public static method (
getInstance()): checks whether the instance exists; if it doesn't, creates and stores it; always returns the same reference.
Calling MyClass.getInstance() multiple times, every call
receives the same object — there are no copies, no duplicated state.
Structure
The diagram below, in simplified UML notation, shows the Singleton class with its essential members:
┌─────────────────────────────────────────────┐
│ <<Singleton>> │
│ ConfigManager │
├─────────────────────────────────────────────┤
│ - instance: ConfigManager (static) │
│ - data: Record<string, string> │
├─────────────────────────────────────────────┤
│ - constructor() (private) │
│ + getInstance(): ConfigManager (static) │
│ + get(key: string): string | undefined │
│ + set(key: string, value: string): void │
└─────────────────────────────────────────────┘
Usage flow:
Client code A Client code B
│ │
│ ConfigManager.getInstance() │ ConfigManager.getInstance()
│ ─────────────────────────────►│
│ │
│ ┌─────────────────────┤
│ │ instance exists │
│ │ → returns the same │
│ └─────────────────────┤
│◄──────────────────────────────│
│ same reference │
Code examples
Example 1 — Naive implementation (not thread-safe)
This is the simplest form of Singleton. In JavaScript and PHP (single-threaded in most cases), it works well for understanding the pattern. Further ahead we discuss the limitations.
class Logger {
// Private static field that holds the single instance.
private static instance: Logger | null = null;
// Private constructor — prevents `new Logger()` outside the class.
private constructor(private readonly prefix: string = "[LOG]") {}
// Global access point: creates on the first call, reuses on the following ones.
public static getInstance(): Logger {
if (Logger.instance === null) {
Logger.instance = new Logger();
}
return Logger.instance;
}
public info(message: string): void {
console.log(`${this.prefix} [INFO] ${message}`);
}
public error(message: string): void {
console.error(`${this.prefix} [ERROR] ${message}`);
}
}
// ── Usage ────────────────────────────────────────────────────
const logA = Logger.getInstance();
const logB = Logger.getInstance();
console.log(logA === logB); // true — same reference
logA.info("Application started");
logB.error("Something went wrong");
// Both calls write to the same logger.
<?php
class Logger
{
// Private static field that holds the single instance.
// Uses `self` (not `static`) because the constructor is private,
// which rules out inheritance — LSP would be incoherent here.
private static ?self $instance = null;
// Private constructor — prevents `new Logger()` outside the class.
private function __construct(
private readonly string $prefix = '[LOG]'
) {}
// Prevents cloning the instance.
private function __clone(): void {}
// Global access point: creates on the first call, reuses on the following ones.
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function info(string $message): void
{
echo "{$this->prefix} [INFO] {$message}" . PHP_EOL;
}
public function error(string $message): void
{
echo "{$this->prefix} [ERROR] {$message}" . PHP_EOL;
}
}
// ── Usage ────────────────────────────────────────────────────
$logA = Logger::getInstance();
$logB = Logger::getInstance();
var_dump($logA === $logB); // bool(true) — same reference
$logA->info('Application started');
$logB->error('Something went wrong');
Example 2 — Singleton with lazy initialization and strong typing
In real applications, a Singleton often encapsulates a resource initialized with parameters (such as a connection string). The pattern below accepts options on the first call and ignores them on subsequent calls.
interface ConfigOptions {
readonly file: string;
readonly environment: "dev" | "staging" | "prod";
}
class ConfigManager {
private static instance: ConfigManager | null = null;
private readonly data: Map<string, string> = new Map();
private constructor(options: ConfigOptions) {
// Simulates loading a configuration file.
this.data.set("env", options.environment);
this.data.set("file", options.file);
console.log(`[Config] Loaded from "${options.file}" (${options.environment})`);
}
// On the first call, `options` is required.
// On subsequent calls, the argument is ignored.
public static getInstance(options?: ConfigOptions): ConfigManager {
if (ConfigManager.instance === null) {
if (!options) {
throw new Error("ConfigManager: options are required on first initialization.");
}
ConfigManager.instance = new ConfigManager(options);
}
return ConfigManager.instance;
}
public get(key: string): string | undefined {
return this.data.get(key);
}
public set(key: string, value: string): void {
this.data.set(key, value);
}
/** Only for tests — allows resetting the instance between test cases. */
public static resetForTests(): void {
ConfigManager.instance = null;
}
}
// ── Usage ────────────────────────────────────────────────────
const cfg = ConfigManager.getInstance({
file: ".env.prod",
environment: "prod",
});
console.log(cfg.get("env")); // "prod"
// Second call without options — returns the existing instance.
const cfg2 = ConfigManager.getInstance();
console.log(cfg === cfg2); // true
<?php
class ConfigManager
{
// Uses `self` (not `static`): a private constructor rules out subclasses,
// so late static binding adds no value and would only cause confusion.
private static ?self $instance = null;
/** @var array<string, string> */
private array $data = [];
private function __construct(
private readonly string $file,
private readonly string $environment
) {
// Simulates loading a configuration file.
$this->data['env'] = $environment;
$this->data['file'] = $file;
echo "[Config] Loaded from \"{$file}\" ({$environment})" . PHP_EOL;
}
private function __clone(): void {}
/**
* On the first call, $file and $environment are required.
* On subsequent calls, the arguments are ignored.
*
* PHP note: in a share-nothing model (default FPM), the instance is
* recreated on every request — there is no state between requests,
* unlike Node.js where the module stays in memory.
*/
public static function getInstance(
string $file = '',
string $environment = ''
): self {
if (self::$instance === null) {
if ($file === '') {
throw new \RuntimeException(
'ConfigManager: $file is required on first initialization.'
);
}
self::$instance = new self($file, $environment);
}
return self::$instance;
}
public function get(string $key): ?string
{
return $this->data[$key] ?? null;
}
public function set(string $key, string $value): void
{
$this->data[$key] = $value;
}
/** Only for tests — allows resetting the instance between test cases. */
public static function resetForTests(): void
{
self::$instance = null;
}
}
// ── Usage ────────────────────────────────────────────────────
$cfg = ConfigManager::getInstance('.env.prod', 'prod');
echo $cfg->get('env') . PHP_EOL; // prod
// Second call without arguments — returns the existing instance.
$cfg2 = ConfigManager::getInstance();
var_dump($cfg === $cfg2); // bool(true)
When to use
- Shared resource with single state: database connection pool, HTTP client with centralized configuration, in-memory cache.
- Logger / audit system: the entire application must write to the same destination, without output fragmentation.
- Configuration loaded once: environment variables, feature flags, application parameters loaded at startup.
- Simple event bus: a single channel that distributes events between subsystems.
When to avoid
-
Domain objects:
Order,User,Productshould never be Singletons — each entity has its own identity. - Services in frameworks with DI: NestJS, Spring, Laravel and similar frameworks already manage the lifecycle of services. Use the container's scope (singleton scope in DI) instead of manually implementing the pattern.
-
When testability matters: Singletons with state that
persists between tests break isolation. If there's no reset method (like
the example's
resetForTests()), prefer dependency injection.
Pros and cons
Pros
- Guarantees a single instance — no duplicated state or inconsistency.
- Consistent global access point, without needing to pass references through parameters.
- Lazy initialization (the instance is only created when first requested), saving resources if the Singleton is never used.
- Easy to locate in the code: just search for
getInstance().
Cons
- Violates the Single Responsibility Principle: the class manages both its business logic and its lifecycle.
- Makes unit testing harder — you need a reset mechanism or advanced mocking techniques.
- Introduces implicit global coupling: any part of the code can access and modify the Singleton's state.
- Concurrency issues in multi-threaded environments (Java, C#, Go) — the check-then-act verification isn't atomic without synchronization.
- Makes dependencies harder to detect: unlike dependency injection, coupling with the Singleton doesn't appear in the constructor signature.
Common pitfalls
1. Disguised global state
A Singleton is, in practice, an object-oriented global variable. Any part
of the system can call Singleton.getInstance() and change
its state. This creates hidden dependencies that don't appear in method
signatures and only surface at runtime — especially painful in large
projects with multiple teams.
2. Makes testing and mocking harder
Warning: Unit tests that depend on a stateful Singleton can interfere with each other if run in the same process instance. Test A's Singleton state "leaks" into Test B.
The classic solution is to add a resetForTests() method (as
in Example 2) or, even better, to refactor the code to accept the
dependency via injection (constructor or parameter) — then in tests you
pass a fake object without needing the Singleton.
3. Concurrency issues (multi-thread) and lifecycle
In TypeScript/Node.js and standard PHP (without pthreads), execution is
single-threaded and the race condition doesn't occur. But in Java, C# or
Go, two threads can pass the if (instance == null) check
simultaneously and create two instances. The solution is to use
synchronization (synchronized, lock, or
static initialization guaranteed by the language).
Warning — PHP vs Node.js: there's a lifecycle difference that confuses a lot of people. Standard PHP follows the share-nothing per request model: each HTTP request starts a process (or worker) from scratch, executes the script and discards everything at the end. This means the Singleton does not persist between requests — it's recreated on every request. There's no state leakage between users, but also no initialization gain between calls. In TypeScript/Node.js, on the other hand, the process is long-lived: the same module is loaded once and stays in memory while the server is up. The Singleton created on the first request survives and is reused by all subsequent requests — which brings caching benefits along with the risks of shared state between concurrent users.
4. Inheritance and subclasses
Singleton and inheritance don't mix well. If ChildLogger extends
Logger, the static instance field may be shared between
parent and child depending on the language, causing unexpected behavior.
With a private constructor (as in the examples above),
inheritance is simply impossible — which makes the use of
static::$instance (late static binding) in PHP unnecessary,
and misleading. If you need an inheritable Singleton, change the
constructor to protected and then adopt
static::$instance with new static() — but be
aware that complexity increases quickly.
5. Serialization and deserialization
In PHP, if the object is serialized and deserialized (e.g. via
unserialize()), a new instance is created, breaking the
Singleton guarantee. The solution is to implement __wakeup()
throwing an exception to forbid deserialization.
Related patterns
Singleton frequently appears together with, or in contrast to, other patterns:
- Factory Method
- Abstract Factory
- Monostate — variation (coming soon)
- Flyweight
- Facade
Monostate is an alternative to Singleton: instead of controlling the instance, all fields are static, so multiple objects share the same state — easier to test, but equally coupled. Flyweight also controls instance creation, but for sharing large numbers of immutable objects. Facade is frequently implemented as a Singleton to provide a simplified access point to a subsystem.