Design Patterns · Creational Patterns
Singleton
Control creation so callers share one instance and can access it globally. State the scope and the coupling that this introduces.
This lesson follows Prototype. Prototype returns a copy. Singleton returns the same cached instance.
Singleton restricts a class to one instance within a defined runtime scope and provides a global access point. These are two responsibilities. The convenience also hides a dependency and makes mutable state shared.
Control creation and reuse one reference
A process has one registry of enabled features. Several services need that registry. An unrestricted constructor could create several conflicting registries. A private constructor and static accessor funnel callers through one cached reference.
class FeatureRegistry {
private static instance: FeatureRegistry | undefined;
private flags = new Map<string, boolean>();
private constructor() {}
static getInstance(): FeatureRegistry {
if (!this.instance) this.instance = new FeatureRegistry();
return this.instance;
}
enabled(name: string): boolean {
return this.flags.get(name) ?? false;
}
set(name: string, enabled: boolean): void {
this.flags.set(name, enabled);
}
}
const a = FeatureRegistry.getInstance();
const b = FeatureRegistry.getInstance();
a === b; // true
a.set("newCheckout", true);
b.enabled("newCheckout"); // true
The first call initializes the instance. Later calls reuse it. The synchronous accessor above has no await between the check and assignment. This example assumes one JavaScript module instance in one execution context. TypeScript private is a compile-time restriction.
Try it
Change the runtime boundary
A.set('newCheckout', true)
B.enabled('newCheckout') → true
a === b → true
Process A sets its flag to true
Process B still reads false
The instances do not share memory
A singleton class does not coordinate servers, workers, browser tabs, or distributed resources.
Protect initialization when execution can overlap
In a multithreaded runtime, two threads can both observe an empty cache and create separate instances. Use the language's initialization guarantees or a correctly synchronized accessor. Do not paste a double-checked lock without understanding the runtime's memory model.
Step through
See the check-then-create race
Both callers see an empty cache
Thread A: instance == null
Thread B: instance == null
The check alone protects nothing.
Both callers construct
Thread A creates object 1
Thread B creates object 2
Caching the last object cannot erase the first object already returned.
Serialize initialization
one caller initializes under the runtime's guarantee
others wait and receive the published instance
The constructor runs once and the completed object becomes visible safely.
Async initialization has its own race
If getInstance() waits for network setup before storing the result, another call can begin setup too. Cache the initialization promise before awaiting it. Decide whether a rejected promise is retained or cleared so a later caller can retry.
Make the dependency visible when possible
A service that calls FeatureRegistry.getInstance() has a dependency its constructor does not reveal. Replacing the registry for an isolated environment becomes harder. A caller can still create one shared registry at application startup and pass it to every service.
interface Flags { enabled(name: string): boolean }
class Checkout {
constructor(private flags: Flags) {}
useNewFlow() { return this.flags.enabled("newCheckout"); }
}
const shared = FeatureRegistry.getInstance();
const checkout = new Checkout(shared);
const preview = new Checkout({ enabled: () => false });
Checkout depends on Flags. It does not require global access. Sharing one instance by application configuration is often enough. Requiring the class itself to enforce uniqueness is a stronger decision.
Check yourself
Can two services share one registry while receiving it through constructor parameters?
Not quite. Global access and shared lifetime are separate decisions.
Correct. Application startup can pass the same reference to both services.
Map the roles
The pattern has only two roles. Most of its design work is in scope, initialization, and testing.
| Role | In this example | Job |
|---|---|---|
| Singleton | FeatureRegistry | Hides its constructor and exposes a static getInstance() that always returns the same cached object |
| Client | any service that calls getInstance() | Reaches the instance only through the accessor, never through new |
Check yourself
Singleton solves two problems at once. Which pair?
Correct. That double duty is why the pattern breaks the single responsibility principle.
Not quite. Neither is part of the pattern. A singleton is often mutable.
Reach for it when exactly one instance must serve every client
- a class must have exactly one instance available to all clients in a scope, such as one database connection pool shared by the whole process
- you need stricter control than a global variable gives. Any code can overwrite a global. Only the singleton class can replace its cached instance.
- you may later need a different limit. All creation goes through
getInstance(), so changing that one method can allow a small fixed number of instances instead.
Check yourself
A global variable registry is reassigned by a test helper, breaking later tests. What does Singleton add over the global?
Not quite. Thread safety needs its own mechanism. See the race above.
Correct. The private constructor and private static field stop outside code from overwriting the instance.
Implement it in five steps
Refactor existing code toward the pattern in this order:
- Add a private static field that holds the instance.
- Declare a public static creation method,
getInstance(), that returns it. - Implement lazy initialization inside it: create the object on the first call, store it in the static field, and return the stored object on every later call.
- Make the constructor private. The static method can still call it. Other code cannot.
- Replace every direct constructor call in client code with a call to
getInstance().
Check yourself
After the refactor, a module still compiles a call to new FeatureRegistry(). Which step was skipped?
Correct. A private constructor turns that stray call into a compile error, so the accessor becomes the only path.
Not quite. The field stores the instance. It does not stop direct construction.
Name what it costs
| You gain | You pay |
|---|---|
| One instance in the stated scope | Breaks single responsibility: it controls its own count and does its real job |
| A global access point to that instance | Can mask bad design, where components know too much about each other |
| Initialized only on first request | Multithreaded or async initialization needs special care so two callers do not create two objects |
| A clear owner for a shared resource | Hard to unit test: test frameworks often mock through subclasses, but the constructor is private and static methods cannot be overridden |
Check yourself
A test wants a fake registry, but every service calls FeatureRegistry.getInstance() directly. What makes the fake hard to inject?
Correct. Passing a Flags interface through constructors, as shown above, makes the dependency visible and replaceable.
Not quite. The data type is irrelevant. The static lookup is the obstacle.
Do not confuse it with its neighbors
| Pattern | How it relates |
|---|---|
| Facade | One facade object is usually enough, so a facade is often made a singleton. |
| Flyweight | If all shared state fit in one flyweight, it would resemble a singleton. Two differences remain: a flyweight class can have many instances with different intrinsic state, and flyweights are immutable while a singleton can be mutable. |
| Abstract Factory, Builder, Prototype | Each can be implemented as a singleton when one instance is enough. |
Check yourself
A pool holds one immutable icon style per icon kind, so there are several styles. Is that a singleton?
Correct. A singleton limits a class to one instance. This pool deliberately keeps one per key.
Not quite. Sharing alone does not make a singleton. The instance count does.
Retrieve and apply
Check yourself
Three servers each have a Singleton job scheduler. Does that guarantee only one server runs a job?
Correct. An in-memory instance limit does not enforce a rule across processes. Cross-server coordination requires a separate mechanism.
Not quite. The constructor only controls creation in its own runtime scope. Other servers have separate memory.
Name a shared dependency in an application. State its required lifetime, who owns cleanup, and whether global access is necessary. Continue to Adapter when an existing object exposes the wrong interface.
Source: Alexander Shvets, Dive Into Design Patterns (深入设计模式), Chinese edition v2021-1.25. Singleton, printed pages 132–139. Explanations, examples, and exercises are adapted for this course.