AcademyDesign Patterns
Singleton pattern
The guarantee that exactly one instance can exist, where it earns its place (database pools, caches, config), the global state it costs, and why it breaks on serverless.
7 min readUpdated 3 exercises
Singleton answers the narrowest question of the five creational patterns: what happens when more than one instance of a class would be a bug, not just a waste.
A Singleton makes a class responsible for its own uniqueness: the constructor's real work runs at most once, and every caller who asks for "the" instance gets back the exact same object.
The problem it solves
Your API needs a PostgreSQL connection pool. A pool holds, say, 10 open connections and hands them out to requests. Now imagine every file that touches the database writes new Pool() for itself.
- Ten files, ten pools, a hundred connections. The database refuses new ones at its limit, and requests start failing under load that one pool would have absorbed.
- Each pool has its own idle connections, so none of them is ever reused by the others.
- The bug does not show up in development, where one file runs at a time. It shows up in production, under traffic, with an error message that says nothing about pools.
The fix is not "be careful". It is to make the second new Pool() impossible, or at least pointless: the class itself hands out one shared instance.
Structure
Three moving parts, and only three:
- A private static field that holds the instance, empty until first use.
- A private constructor, so nobody outside the class can call
newand skip the check. - A static accessor (
getInstance()) that creates the instance on the first call and returns the same one after.
class Logger {
static #instance;
#lines = [];
static getInstance() {
if (!Logger.#instance) Logger.#instance = new Logger();
return Logger.#instance;
}
log(line) {
this.#lines.push(line);
}
}The lazy check (create on first use, reuse after) is the whole pattern. Skip it and call new Logger() from two places, and you have two loggers silently diverging, which is the exact bug this exists to prevent. The TypeScript version goes one step further: a private constructor makes the compiler refuse a stray new Logger() from outside the class.
Where it is used in real systems
| Use case | Why exactly one |
|---|---|
| Database connection pool | Connections are a limited resource on the database side. Many pools multiply the count until the server refuses new ones. |
| Redis or cache client | One client keeps one connection and one in-memory view. It is cheap to share and wasteful to duplicate. |
| In-memory cache | Two caches mean two versions of the truth: a write to one is invisible to readers of the other. |
| Configuration | Read the environment once, validate it once, and every module sees the same values. |
| Logger | One sink, ordered output, one place to change the log level for everyone. |
| Feature flags | All code paths must agree on whether a flag is on. Two copies can disagree mid-request. |
| Rate limiter state | Counters have to be shared, or each copy lets the full quota through. |
In each row the requirement is the same: sharing is the whole point, and a second instance would give wrong answers or exhaust a resource. If sharing is not the point, you probably do not need a Singleton.
Shared sessions: where the pattern stops working
A very common instinct is to keep user sessions in a Singleton SessionStore so every request can look them up. It works on your laptop. In production it breaks, and this is worth being able to explain in an interview.
A Singleton guarantees one instance per process. It says nothing about the second server you deployed behind the load balancer, or the fifty short-lived instances a serverless platform spins up.
- User logs in on server A, the session lands in A's memory.
- The next request goes to server B, which has its own Singleton and has never heard of that session. The user is logged out.
- On serverless, instances also disappear between requests, taking the in-memory state with them.
The fix is to move the shared state out of the process into something every instance talks to: Redis, or the database. The Singleton then only owns the client to that store, which is the good use of the pattern: one connection object, not the data itself.
Singletons you already use
You rarely write this pattern by hand, because platforms provide it.
- Node.js modules. A module is evaluated once and cached, so
export const prisma = new PrismaClient()is a Singleton with no class at all. Every file that imports it gets the same object. - Prisma in Next.js dev. Hot reload re-evaluates modules and would open a new client on every save. The standard fix stores the client on
globalThisso it survives reloads: a Singleton implemented by hand, for exactly the reason the pattern exists. - Angular.
@Injectable({ providedIn: 'root' })gives one instance for the whole app. The framework owns the uniqueness, and classes just ask for the service. - NestJS. Providers are singleton-scoped by default: one instance per application, shared by every consumer.
- Pinia and Redux stores. One store per app, created once at startup.
The pattern from the top of this page is the manual version. In most modern code, the language, the framework or the module system does the job, and you get the guarantee without the ceremony.
When not to use it
Singleton has a real cost, and it is the reason the pattern has a bad reputation:
- Global mutable state. Any code can reach in and change it, and nothing in a function signature tells you it does. Bugs then show up far from their cause.
- Hidden dependencies. A function that calls
Logger.getInstance()depends on the logger, but its parameters say nothing about it. - Hard to test. State leaks from one test to the next, and you cannot swap in a fake without reaching into the class.
- Concurrency. In Java or C#, two threads can both pass the
if (!instance)check and create two objects. That needs a lock, or double-checked locking. Node is single-threaded, so the check is safe there, but the moment you use worker threads or multiple processes you are back to one instance per thread or process.
The usual alternative is dependency injection: create the single instance once at startup and pass it to whoever needs it.
// Instead of Logger.getInstance() buried inside the class,
// the dependency is visible and replaceable in a test.
class OrderService {
constructor(private readonly logger: Logger) {}
}
const logger = new Logger(); // still exactly one, created once
const orders = new OrderService(logger);You keep "exactly one" and lose the global access point. That is usually a better trade.
What interviewers ask
- "Implement a Singleton." Lazy
getInstance(), private constructor. Then expect the follow-up below. - "Is it thread-safe?" In Java, no, not as written: explain the race between the check and the assignment, and the fix (
synchronized, double-checked locking withvolatile, or an eager instance). In Node, say why it is not an issue, and where it comes back (worker threads). - "Why is Singleton called an anti-pattern?" Global state, hidden dependencies, testing. Then give the alternative: dependency injection.
- "Give a real use case." Connection pool or Redis client. Add unprompted that in-memory session data is the wrong use, because it is per process.
- "How would you test code that uses one?" Inject it, or expose a reset for tests. Better, avoid the global access in the first place.
The mistake to avoid
Making everything a Singleton "to be safe". Global state is the cost, not the feature. Reach for it because sharing is the actual requirement, and prefer creating the one instance yourself and passing it in.
Singleton is one naming device among several an object-oriented language gives you for controlling how
newgets called. The mechanism is the same, syntax aside, whether the language is JavaScript, Python or Java.
Now practice it
1 challenge · 2 quizzesReading this page is the cheap half. These are the exercises that make you use it.
- SingletonProImplement the lazy getInstance() check yourself and prove two references share state — the guard this page walks through, made runnable.ChallengeSolve · EasyChallengeSolve · Easy
- Design Patterns · Creational PatternsTen quick questions across Singleton, Factory Method, Abstract Factory, Builder and Prototype, to check the mental model before this one.QuizTake · EasyQuizTake · Easy
- Design Patterns · Creational Patterns — Series 2ProA second, tougher pass on the same creational family, for once the first set feels easy.QuizTake · EasyQuizTake · Easy
Read next
Factory Method pattern
Hiding which concrete class gets built behind one function, the indirection it costs when the choice might never grow, and the leak when a return type still names the class.
7 min
Builder pattern
Replacing a constructor drowning in optional parameters, the extra code it costs for objects simple enough not to need it, and build() handing back the same object twice.
6 min