Skip to content

# The Singleton Pattern: Testing, Concurrency, and Clusters

Singleton is a way to make sure only one instance of something exists in your program and if you try to create another, you’ll just get back the first one thus avoiding problems that come from having too many versions of the same thing running in your app. However singleton can cause test flakiness (where sometimes passes, sometimes fails, even though you didn’t change the code) , concurrency bugs (where multiple things happen at the same time (like async requests), they can step on each other and cause unexpected behavior) and Cluster Issues (where singleton applies only within a single process but modern apps rarely run as a single process) Here we will look at what the singleton pattern is in Javascript, how useful it is but also some of the pitfalls it can cause and how to overcome them.

Why is this Singleton useful

  • Sometimes creating new things is expensive (like opening a new database connection).
  • Sometimes you need consistency (you don’t want two loggers writing different formats).
  • Sometimes duplication is unsafe (two caches with different values is a recipe for bugs). Singletons fix the problem with uncontrolled globals by acting as a controlled global — one instance, explicitly managed. With globals we can say we are leaving our house keys under the doormat, while singleton is like having a secure lock with a single copy of the key

TIP

if multiple copies would cause trouble, a singleton makes sure there’s just one

Singleton example: Database Pooling

A database pool is like a taxi rank(A queue area of the street where taxicabs line up to wait for passengers). So;

  • Instead of everyone buying their own car (expensive DB connection), there’s a pool of taxis (connections) waiting.
  • When you need one, you borrow a taxi, do your trip (query), and return it to the rank. This way, 100 passengers can share 10 taxis, without pooling, everyone would buy their own car which could lead to traffic jam (too many DB connections).

Singleton database example

js
// db.js
import { Pool } from "pg"; // PostgreSQL client

class Database {
  // keep a single shared instance
  static #instance;

  constructor(connectionString) {
    // If we already have an instance, return it instead of making a new one
    if (Database.#instance) return Database.#instance;

    // Otherwise, create a new connection pool
    this.pool = new Pool({ connectionString });

    // Save this instance so the next call reuses it
    Database.#instance = this;
  }

  // Method to run queries
  async query(sql, params) {
    return this.pool.query(sql, params);
  }

}

export default Database;

Lets us use the above database class in two different modules

js
// users.js
import Database from "./db.js";

// Both modules will end up using the same pool instance
const db = new Database(process.env.DB_URL);

export async function getUsers() {
  return db.query("SELECT * FROM users");
}

and

js
// orders.js
import Database from "./db.js";

const db = new Database(process.env.DB_URL);

export async function getOrders() {
  return db.query("SELECT * FROM orders");
}

Note

Even though we called new Database(...) in both modules, they share one pool under the hood.

Singleton example: Database Pooling - Challenges and fixes

1. Test flakiness Tests reuse the same Database singleton. One test may insert rows or keep state that bleeds into the next test thus giving unpredictable results.

  • Fix Add _reset method on the class Without reset, tests may become flaky because state leaks between runs
js
 // Special helper for tests — allows resetting the singleton
  static _reset() {
    Database.#instance = null;
  }

thus

js
// users.test.js
import Database from "./db.js";

beforeEach(() => {
  Database._reset(); // ensures no state leaks across tests
});

2. Concurrency Bugs

In production, multiple requests may run at the same time. If they all share one Database instance, that’s good (no duplicate pools), but you still need to ensure queries don’t step on each other. Here pg.Pool library already handles concurrency by managing a queue of queries. Multiple .query() calls are safe — the pool assigns them to available connections. However, here we don’t add manual locking as pg.Pool handles it, also if we need atomic work (e.g., transfer money between accounts), we use transactions:

js
async function transferFunds(db, fromId, toId, amount) {
  const client = await db.pool.connect();
  try {
    await client.query("BEGIN");
    await client.query("UPDATE accounts SET balance = balance - $1 WHERE id = $2", [amount, fromId]);
    await client.query("UPDATE accounts SET balance = balance + $1 WHERE id = $2", [amount, toId]);
    await client.query("COMMIT");
  } catch (err) {
    await client.query("ROLLBACK");
    throw err;
  } finally {
    client.release();
  }
}

3. Cluster Issues The singleton only applies inside one Node.js process. If you run multiple containers or processes mainly in production, each one creates its own pool. If each pool has 10 connections and you run 10 replicas that is 100 DB connections which may overwhelm Postgres.

  • Fix Control pool size Limit pool size so even across multiple processes you don’t blow past DB limits:
js
this.pool = new Pool({
  connectionString,
  max: 5, // each process only opens 5 connections
});
  • Fix 2 PgBouncer Put PgBouncer or RDS Proxy in front of Postgres. That way your many app processes share a smaller, smarter connection pool at the infrastructure layer.

  • Fix 3 Use distributed coordination If we want singleton job runner (like “clean up old sessions once”), we can’t rely on Database singleton as each process will think “I’m the only one.” Instead we can use:

  • A Redis-backed lock (Redlock)

  • Or a message queue (Kafka, RabbitMQ)

  • Or leader election (Kubernetes lease)

javascript book

If this interested you, check out my Javascript Book

Enjoy every byte.