codejump

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

Waiting at the end of this page.

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

Singleton keeps its one instance in a private static field and exposes it through getInstance()
Singleton keeps its one instance in a private static field and exposes it through getInstance()

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 new and 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

Three different entry points share one database pool, and through it one set of connections
Three different entry points share one database pool, and through it one set of connections
Use caseWhy exactly one
Database connection poolConnections are a limited resource on the database side. Many pools multiply the count until the server refuses new ones.
Redis or cache clientOne client keeps one connection and one in-memory view. It is cheap to share and wasteful to duplicate.
In-memory cacheTwo caches mean two versions of the truth: a write to one is invisible to readers of the other.
ConfigurationRead the environment once, validate it once, and every module sees the same values.
LoggerOne sink, ordered output, one place to change the log level for everyone.
Feature flagsAll code paths must agree on whether a flag is on. Two copies can disagree mid-request.
Rate limiter stateCounters 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 globalThis so 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.

TypeScript
// 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 with volatile, 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 new gets called. The mechanism is the same, syntax aside, whether the language is JavaScript, Python or Java.

Now practice it

1 challenge · 2 quizzes

Reading this page is the cheap half. These are the exercises that make you use it.