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.

Your answer

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

The picture to keep
The stadium scoreboard

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.

In one line: One instance, created once, and everyone who asks gets that same one.

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.

Figure 1. Three services ask for the pool and receive the same object, so the database sees ten connections and not thirty.

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()) // 1

No 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 = 42

Look 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 = 42

There 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.

ChoiceWhat you gainWhat you payPick 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 valueThree 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 everywhereOne 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.
Is your singleton thread safe?

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()) // true

The first caller stores the promise before anything is awaited. The second caller gets that same promise and waits on it.

TypeScript you just picked up
private constructor(...)
Nobody outside the class can call new. The class controls its own creation.
private static instance: ConnectionPool | undefined
A field on the class itself. One slot for the whole program, empty at first.
static getInstance(): ConnectionPool
Called on the class, as ConnectionPool.getInstance(), with no object needed.
export const pool = new ConnectionPool(10)
A module level value. Evaluated once, then shared by every import.
x ??= y
Assign y to x only if x is null or undefined. Short for a three line if.
Promise<Database>
A promise that will produce a Database. async functions always return one.

Checkpoint

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?

Say this in 60 seconds

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.

IndGeek provides solutions in the software field, and is a hub for ultimate Tech Knowledge.