Singleton
Fourteen files in the backend needed the database, and each one did
new ConnectionPool() at the top. Each pool opened 10 connections. Postgres allows 100
by default. In development, where only a few of those files were ever loaded, everything
worked. In production, on the first busy evening, the eleventh pool asked for its
connections and Postgres answered “sorry, too many clients already”.
There was supposed to be one pool. Nothing in the code said so.
You need exactly one ConnectionPool in the whole application, and any file should be able to use it. How would you guarantee that a second one can never be created by accident?
The idea
Forty thousand people, one scoreboard. Nobody brings their own. If the east stand had a second board showing a different score, there would be a fight by the tenth over. One board works because everyone is looking at the same object.
A singleton is a class that can have only one object, plus a well known way to get it.
The classic form
class ConnectionPool {
private static instance: ConnectionPool | undefined
private inUse = 0
private constructor(readonly size: number) {}
static getInstance(): ConnectionPool {
if (!ConnectionPool.instance) {
ConnectionPool.instance = new ConnectionPool(10)
}
return ConnectionPool.instance
}
acquire(): number {
this.inUse += 1
return this.inUse
}
}
const a = ConnectionPool.getInstance()
const b = ConnectionPool.getInstance()
console.log(a === b) // true
console.log(a.acquire()) // 1
console.log(b.acquire()) // 2
const c = new ConnectionPool(10)
// Error: Constructor of class 'ConnectionPool' is private and only
// accessible within the class declaration.Three parts do the work.
private constructor means new ConnectionPool() is a compile error everywhere outside
the class. That is the guarantee.
static puts a member on the class itself, not on each object. So there is one
instance slot for the whole program, and you call ConnectionPool.getInstance()
without having an object first.
getInstance creates the object the first time it is asked and hands back the same one
every time after. Creating it on first use is called lazy initialisation. a and b
are the same object, which is why b.acquire() returned 2.
The TypeScript way: a module
In Node and in every bundler, a module’s top level code runs once. After that, every
import of the file gets the same cached exports. So the simplest singleton is an
exported value.
class ConnectionPool {
private inUse = 0
constructor(readonly size: number) {}
acquire(): number {
this.inUse += 1
return this.inUse
}
}
// pool.ts exports one instance. Every importer shares it.
export const pool = new ConnectionPool(10)
console.log(pool.acquire()) // 1No static field, no getInstance, no private constructor. The module system already
gives you “created once, shared by all”. This is what most TypeScript codebases do, and
you have probably written it without calling it a pattern.
The class form still has two uses: when the object is expensive and must not be created until someone needs it, and when an interviewer asks you to write a singleton.
Why people argue about it
Singleton is the most criticised pattern in the book, and the criticism is fair.
class ConnectionPool {
private static instance: ConnectionPool | undefined
static getInstance(): ConnectionPool {
ConnectionPool.instance ??= new ConnectionPool()
return ConnectionPool.instance
}
query(sql: string): string {
return `ran: ${sql}`
}
}
class OrderService {
findOrder(id: number): string {
const pool = ConnectionPool.getInstance()
return pool.query(`SELECT * FROM orders WHERE id = ${id}`)
}
}
console.log(new OrderService().findOrder(42))
// > ran: SELECT * FROM orders WHERE id = 42Look at OrderService. Its constructor takes nothing, so from outside it appears to need
nothing. In fact it needs a live database, and you only find out by reading the method
body. You cannot hand it a fake in a test. This is the welded dependency from the
Dependency inversion page, with a pattern name
on it.
There is a second problem. A singleton is a global variable. One test leaves 3 connections acquired and the next test starts with 3, so they pass or fail depending on the order they run in.
The fix keeps the single instance and drops the global access:
interface Database {
query(sql: string): string
}
class ConnectionPool implements Database {
query(sql: string): string {
return `ran: ${sql}`
}
}
class OrderService {
constructor(private readonly db: Database) {}
findOrder(id: number): string {
return this.db.query(`SELECT * FROM orders WHERE id = ${id}`)
}
}
// main.ts creates one pool and passes it to everything that needs it.
const pool = new ConnectionPool()
const orders = new OrderService(pool)
console.log(orders.findOrder(42)) // ran: SELECT * FROM orders WHERE id = 42There is still exactly one pool, because main.ts creates exactly one. Having a single
instance was never the problem. Letting every class reach for it from anywhere was.
| Choice | What you gain | What you pay | Pick it when |
|---|---|---|---|
| Class with getInstance() | A second instance cannot be created, and creation is delayed until first use. | Global state, hidden dependencies, and tests that affect each other. | An interview asks for it, or you need lazy creation and cannot inject. |
| Exported module value | Three lines, no ceremony, and the module cache gives you one instance for free. | Still global. Created at import time, whether or not anyone uses it. | Config, a logger, small stateless helpers. |
| Create once, inject everywhere | One instance in production, and any test can pass its own fake. | You have to pass it through constructors, which is more wiring. | Anything that touches a database, the network or shared mutable state. |
A Java interviewer asks this to hear about double checked locking. JavaScript runs your
code on one thread, so two callers cannot be inside getInstance at the same moment and
the plain version is safe. Say that, and then give the JavaScript version of the
problem: async creation. If getInstance awaits a connection, two callers can both see
“no instance yet” before the first await finishes, and you get two. The fix is to store
the promise, not the result.
class Database {
private static ready: Promise<Database> | undefined
private constructor() {}
static connect(): Promise<Database> {
Database.ready ??= Database.open()
return Database.ready
}
private static async open(): Promise<Database> {
// ...await the real connection here
return new Database()
}
}
console.log(Database.connect() === Database.connect()) // trueThe first caller stores the promise before anything is awaited. The second caller gets that same promise and waits on it.
private constructor(...)private static instance: ConnectionPool | undefinedstatic getInstance(): ConnectionPoolexport const pool = new ConnectionPool(10)x ??= yPromise<Database>Checkpoint
1. In the classic singleton, what stops another file from creating a second instance?
2. Why is "export const pool = new ConnectionPool()" already a singleton?
3. OrderService calls ConnectionPool.getInstance() inside its methods. What is the main cost?
A singleton makes sure a class has exactly one instance and gives everyone a way to reach it. The classic form is a private constructor, a static field holding the instance, and a static getInstance that creates it on first use. In TypeScript the lighter form is an exported module value, because modules are evaluated once and cached. I use it for things that must be shared, like a connection pool or config. The cost is that it is global state: it hides dependencies and lets tests interfere with each other. So for anything that touches a database or the network, I create one instance at startup and inject it, which keeps the single instance without the global access. And in JavaScript the concurrency issue is not threads, it is async creation, which I solve by caching the promise.
