Singleton Design Pattern
I created three database connection pools in the same application. Each module imported the config and spun up its own pool. We hit the connection limit f...
13 Mar 2024

I created three database connection pools in the same application. Each module imported the config and spun up its own pool. We hit the connection limit fast. The fix was embarrassingly simple: make one pool and share it everywhere.
The Singleton pattern guarantees a class has exactly one instance and provides a global access point to it. That's it. One instance. One way to get it.
Think of it like a country's president. There's only one at a time. Everyone refers to the same person. You don't create a new president when you need one — you access the existing one.
const Singleton = (function () {
let instance;
function createInstance() {
return {
connection: "db://localhost:5432",
query(sql) {
console.log(`Executing: ${sql}`);
},
};
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
},
};
})();
const db1 = Singleton.getInstance();
const db2 = Singleton.getInstance();
console.log(db1 === db2); // true — same instance
db1.query("SELECT * FROM users");
The IIFE creates a closure. instance is private. getInstance() creates the instance on first call and returns the same one on every subsequent call. No way to create a second instance.
The Modern Way: ES Modules
In modern JavaScript, ES modules are singletons by default. The module is evaluated once. Every file that imports it gets the same reference.
// db.js
class Database {
constructor() {
this.connection = "db://localhost:5432";
}
query(sql) {
console.log(`Executing: ${sql}`);
}
}
export default new Database();
// Any file
import db from './db.js';
db.query("SELECT * FROM users"); // Same instance everywhere
No special pattern needed. The module system handles it.
When Singleton Makes Sense
- Database connection pools. You want one pool, not one per module.
- Logger instances. One logger with consistent configuration across the app.
- Configuration objects. Loaded once, read everywhere.
- Caches. A single shared cache.
When It Causes Problems
- Testing. Singletons carry state between tests. One test modifies the singleton, the next test sees the modified state. You need explicit reset mechanisms.
- Hidden dependencies. When a function calls
Singleton.getInstance()internally, its dependencies are invisible. You can't tell from the function signature what it depends on. - Concurrency. In multi-threaded environments (not typical JS, but relevant in worker threads), lazy initialization can create race conditions.
The benefit: Guaranteed single instance. Controlled access. Lazy initialization — the instance isn't created until first use.
The cost: Global state by another name. Makes testing harder. Hides dependencies. Can become a dumping ground for unrelated shared state. If you find yourself putting multiple unrelated things into a singleton, you have multiple singletons pretending to be one.
I use singletons for infrastructure concerns — connections, caches, loggers. For business logic, dependency injection is almost always a better choice. Pass what you need explicitly rather than reaching for a global instance.
Keep reading
- Event Sourcing Checklist: When It Makes Sense & Common Pitfalls
- Error Boundary React Design Pattern
- Context API React Design Pattern
- Higher-Order Component (HOC) React Design Pattern
- Container Component React Design Pattern
- Best Practices for Structuring Express.js Applications with Prisma Design Pattern