DEV Community

Manoj Khatri
Manoj Khatri

Posted on

The Everyday Backend Engineer: Step 1 — The Singleton Pattern

Welcome to the first official technical post of our series, The Everyday Backend Engineer: Practical Design Patterns. We are kicking things off with a Creational Pattern that every backend developer uses (or should be using) to protect system resources and prevent server crashes: The Singleton Pattern.

Let’s break it down using a practical backend approach: the problem, the real-world analogy, and a clean Node.js solution.


🔴 The Problem: Database Overload

Imagine you are building an Express.js API, and every time a user hits a route to fetch data, you spin up a brand new database connection inside the controller or service file:

// ❌ Bad Practice: Creating a new connection on every request
app.get('/users', async (req, res) => {
    const db = new DatabaseConnection(); 
    const users = await db.query("SELECT * FROM users");
    res.json(users);
});

Enter fullscreen mode Exit fullscreen mode

What happens under heavy traffic?

If 1,000 users hit this endpoint at the exact same second, your application will attempt to open 1,000 independent, concurrent connections to your database.

Databases have strict structural limits on how many concurrent connections they can handle (e.g., a max limit of 100). Once you breach that limit, your database will reject new incoming requests, throwing a fatal "Database Connection Limit Exceeded" error and crashing your entire backend.


🟢 The Solution: A Single Global Instance

We don't need a fresh database connection instance for every file or request. We just need one permanent, shared instance that initializes when the server starts and stays alive until the process shuts down. Every module across our app should share this exact same instance.

This is the core philosophy of the Singleton Pattern: Ensure a class has only one instance, and provide a global point of access to it.

📺 The Real-World Analogy: "The Shared TV"

Think of a house with five family members (modules). Instead of buying five separate televisions and stuffing them into five different bedrooms (wasting money, space, and electricity), the family places one single TV in the living room. Everyone walks to the living room and shares that exact same television instance.


💻 The Clean Node.js Implementation

In traditional object-oriented languages like Java or C#, implementing a Singleton requires private constructors and static methods.

In Node.js, we can leverage the native behavior of the CommonJS/ES Module Caching System. When you use require() or import on a file for the first time, Node.js executes the code, caches the exported result in memory, and returns that exact cached result for every subsequent call.

Here is how to set up a database pool instance as a Singleton:

// config/database.js
const { Pool } = require('pg');

class DatabaseConfig {
    constructor() {
        // Create a connection pool that manages a maximum of 10 concurrent connections
        this.pool = new Pool({
            host: 'localhost',
            user: 'db_user',
            max: 10 // Max 10 parallel independent tasks allowed
        });

        console.log("🚀 Database connection pool initialized for the FIRST time!");
    }

    // A simple wrapper method to execute queries
    query(text, params) {
        return this.pool.query(text, params);
    }
}

// Step 1: Create the SINGLE instance right here inside the module
const databaseInstance = new DatabaseConfig();

// Step 2: Freeze the object to prevent external code from modifying or adding properties
Object.freeze(databaseInstance);

// Step 3: Export the frozen instance, NOT the class
module.exports = databaseInstance;

Enter fullscreen mode Exit fullscreen mode

🎯 How to use it cleanly across your application

Now, no matter how many different files import your database configuration, the constructor will only run once, and they will all share the exact same instance.

// controllers/userController.js
const db = require('../config/database'); // Returns the cached global instance

async function getAllUsers(req, res) {
    // This safely borrows a connection from the existing pool of 10
    const result = await db.query("SELECT * FROM users");
    res.json(result.rows);
}

Enter fullscreen mode Exit fullscreen mode
// controllers/productController.js
const db = require('../config/database'); // Returns the EXACT SAME cached instance as above

async function getAllProducts(req, res) {
    const result = await db.query("SELECT * FROM products");
    res.json(result.rows);
}

Enter fullscreen mode Exit fullscreen mode

🧠 Code Smell Test: When should you use a Singleton?

Keep an eye out for this indicator during code reviews:

  • The Smell: You see multiple files initializing the exact same resource manager using the new keyword (e.g., new RedisClient(), new ConfigManager()).
  • The Fix: Move the instantiation inside the config file, freeze the object instance using Object.freeze(), and export it directly.

That wraps up Step 1! Your database is now safely protected behind a memory-efficient Singleton instance.

Up next in our blog series is Step 2: The Factory Pattern, where we will look at how to cleanly instantiate different objects without making our core business logic file messy with nested if/else blocks.

Have you used singletons in Node.js using module exports, or do you prefer class-based structural controls? Let me know in the comments below!

Top comments (0)