You want exactly one database connection pool for the whole process. Creating a second would open twice the connections and quietly exhaust the server’s limit.
Singleton is the pattern that guarantees one — and it is the most criticised entry in the catalogue. Both facts are worth understanding, because “one instance” is a legitimate requirement and Singleton is not the only way to get it.
The pattern
class ConnectionPool { private static instance: ConnectionPool | undefined;
private constructor() { /* expensive setup */ }
static get(): ConnectionPool { return (ConnectionPool.instance ??= new ConnectionPool()); }}
ConnectionPool.get().query('…');Private constructor, static accessor, lazily created on first use.
It is doing two separate things
This is the crux, and separating them is what makes the criticism make sense:
- There is exactly one instance. Usually a real requirement.
- Anyone, anywhere, can reach it without being given it. Almost never a requirement — it is a convenience that came along for the ride.
Every complaint about Singleton is a complaint about the second one.
class Database {
private static instance: Database;
private constructor() {}
static get(): Database {
return (Database.instance ??= new Database());
}
}
// Somewhere far away, with no hint in the signature:
function saveOrder(order: Order) {
Database.get().insert(order);
} Nothing in the signature says this touches a database. Two tests running together share one connection pool, and there is no way to substitute a fake.
The other failure modes
Tests share state. A singleton persists across test cases in the same process. Test A leaves a cached value, test B passes locally and fails in CI where the order differs. This is the classic flaky test, and it is very hard to attribute to its cause.
Initialisation order is unpredictable. Lazily created “on first use” means created wherever the first use happened to be — which changes when you reorder imports.
“One per process” is often the wrong scope. The requirement is usually one per request, per tenant, or per test. A singleton makes the coarsest possible choice, permanently.
What to do instead
Create one, pass it down. Construct it at the top of your program and hand it to whatever needs it. This is dependency injection, and it needs no framework:
const db = new Database(process.env.DATABASE_URL!);const orders = new OrderService(db);const app = createServer(orders);One instance — enforced by only writing new once, which is enough.
In JavaScript, use a module. Module bodies evaluate once and the result is cached, so this is already a singleton with no pattern:
export const db = new Database(process.env.DATABASE_URL!);Note that this trades away the testability again, so prefer exporting a factory if the module is used from tests.
Use a DI container if the graph is large enough that hand-wiring hurts. Most of them support a “singleton” lifetime, which gives you one instance without a global.
When it is genuinely fine
Not never. Singleton is reasonable when the object is stateless or immutable, so nothing can leak between callers:
- A logger that writes to a file
- A configuration object loaded once at boot and never mutated
- A constant lookup table
- A metrics registry, where a global is the point
The test is simple: if two parts of the program used different instances, would anything break? If not, you do not need to enforce one. If yes, ask whether the shared thing is mutable — because that is where the trouble starts.