Coding · Design patterns
Singleton pattern explained with examples
Short answer
Singleton is a creational design pattern that ensures a class has only one instance and provides a global point of access to it. In TypeScript you implement it with a private constructor and a static method that creates the instance on first use and returns the same object afterwards. It is useful for truly unique resources, such as a configuration object or a connection pool, but it acts like global state, so many teams prefer passing a single shared instance explicitly.
Intent
Ensure a class has only one instance, and provide a global point of access to it. Singleton is one of the five creational patterns in the Gang of Four (GoF) book.
The problem it solves
Some objects must exist exactly once in a program. Imagine an application configuration loaded from environment variables, an in-process cache, or a pool of database connections. If every module called new Config(), each would read the environment again, the cache would be split into many copies, and the pool limit would be meaningless.
You need two things: a guarantee that a second instance cannot be created by accident, and an easy way for any code to reach the one instance.
Structure and participants
| Participant | Responsibility |
|---|---|
| Singleton class | Hides its constructor so other code cannot call new directly. |
| Static instance field | Holds the single instance once it has been created. |
Static access method (often getInstance) | Creates the instance on first call (lazy initialization) and returns the same object on every call. |
| Client | Gets the instance only through the access method. |
TypeScript implementation
A private constructor makes new AppConfig() a compile-time error outside the class. The static method creates the object lazily:
class AppConfig {
private static instance: AppConfig | undefined;
readonly settings: Record<string, string>;
private constructor() {
console.log("loading config once");
this.settings = { mode: "production", region: "eu" };
}
static getInstance(): AppConfig {
if (!AppConfig.instance) {
AppConfig.instance = new AppConfig();
}
return AppConfig.instance;
}
}
const a = AppConfig.getInstance(); // loading config once
const b = AppConfig.getInstance();
console.log(a === b); // true
console.log(a.settings.mode); // production
// new AppConfig(); // error: constructor is privateIn JavaScript and TypeScript, an ES module is evaluated once per program, so exporting an instance from a module is the simplest singleton. Every import receives the same object:
// logger.ts
class Logger {
private lines: string[] = [];
log(msg: string): void { this.lines.push(msg); console.log(msg); }
count(): number { return this.lines.length; }
}
export const logger = new Logger();
// elsewhere: import { logger } from "./logger";
logger.log("started"); // started
console.log(logger.count()); // 1Async initialization
If creating the instance is asynchronous, such as opening a database connection, two callers can both see "no instance yet" and start two connections before the first one finishes. Cache the promise, not the result:
class Database {
private static ready: Promise<Database> | undefined;
private constructor(readonly url: string) {}
static get(): Promise<Database> {
if (!Database.ready) {
Database.ready = (async () => {
await new Promise((r) => setTimeout(r, 10)); // simulate connecting
console.log("connected once");
return new Database("db://main");
})();
}
return Database.ready;
}
}
const [x, y] = await Promise.all([Database.get(), Database.get()]);
console.log(x === y); // trueIn multi-threaded languages such as Java and C#, lazy creation needs synchronization for the same reason. Common solutions are eager initialization in a static field, a lazy holder class, or, in Java, a single-element enum.
When to use it, and when not to
Use Singleton when:
- There must be exactly one instance for correctness, not only for convenience (one hardware device, one process-wide registry).
- Creation is expensive and the object is stateless or thread-safe after creation.
Avoid it, or limit it, when:
- You mainly want convenient global access. A singleton is global mutable state with a nicer name: any code can change it, and the dependency is hidden from function signatures.
- You need tests. Tests that share a singleton affect each other, and it is hard to replace the real instance with a fake.
- The "one instance" rule may change later, for example when you need one cache per tenant.
The usual alternative is dependency injection: create one instance at the start of the program (the composition root) and pass it to the classes that need it. You still have one instance, but the dependency is visible and replaceable.
class Cache {
private data = new Map<string, string>();
get(key: string): string | undefined { return this.data.get(key); }
set(key: string, value: string): void { this.data.set(key, value); }
}
class UserService {
constructor(private cache: Cache) {} // dependency is explicit
remember(id: string, name: string): void { this.cache.set(id, name); }
}
const sharedCache = new Cache(); // the single instance, created once
const users = new UserService(sharedCache);
users.remember("u1", "Ada");
console.log(sharedCache.get("u1")); // AdaReal-world uses
- Java:
Runtime.getRuntime()returns the singleRuntimeobject for the running Java application. - Node.js caches modules after the first load, so a module's exported object is shared by every file that requires or imports it.
- Browsers expose unique global objects such as
windowanddocumentfor each page. - Frameworks with dependency injection, such as Angular and Spring, create "singleton-scoped" services by default: one instance per injector or container, without the static-access part of the pattern.
Related patterns
- Abstract Factory, Builder and Prototype objects are often implemented as singletons, because one factory is enough.
- Facade objects are frequently single instances, since one entry point to a subsystem is usually enough.
- Flyweight also shares objects, but it manages many shared instances keyed by their state, while Singleton has exactly one.
- Dependency injection, which is not a GoF pattern, is the most common replacement for Singleton in modern code.
FAQ
Is Singleton an anti-pattern?
It is often called one, because it introduces hidden global state and makes testing harder. Having one instance is fine; the problem is the global access. Creating one instance and passing it explicitly avoids most of the issues.
How do I make a singleton in TypeScript?
Use a private constructor, a private static field for the instance, and a static method that creates the instance on first call. Exporting a single instance from a module is a simpler alternative.
Is a JavaScript module a singleton?
Effectively yes. A module is evaluated once and its exports are shared by every importer in the same program, unless the same code is loaded from two different paths.
How do I test code that uses a singleton?
Make the singleton implement an interface and pass it into the code under test, so tests can supply a fake. If you cannot change the design, add a reset method that is used only in tests.
What is the difference between a singleton and a static class?
A singleton is a real object: it can implement interfaces, be passed as a parameter and be created lazily. A class with only static members cannot be substituted or passed around as an object.