Back to Home

Singleton Pattern in Node.js: A Simple and Efficient Implementation


singleton


In this post, we'll explore a straightforward implementation of the singleton pattern in Node.js, designed to ensure a single instance of a resource is created and reused throughout an application. This pattern is especially useful for managing shared resources like database connections, configuration objects, or other costly-to-create entities.


What is a Singleton?

A singleton is a design pattern that ensures a class or function creates only one instance of an object and provides a global point of access to that instance. It's a common approach to managing shared resources efficiently, as it avoids redundant instantiations.


The Singleton Function

Here’s a simple yet effective implementation of the singleton pattern in Node.js:

function singleton<Value>(name: string, value: () => Value): Value {
  const globalAny: any = global;
  globalAny.__singletons = globalAny.__singletons || {};

  if (!globalAny.__singletons[name]) {
    globalAny.__singletons[name] = value();
  }

  return globalAny.__singletons[name];
}

How It Works

  1. Global Object as Storage:

  2. Lazy Initialization:

  3. Reusable Across Calls:


Function Parameters


Example Usage

Let’s see how this singleton function can be used to manage a database connection:

const dbConnection = singleton('dbConnection', () => {
  console.log('Creating database connection...');
  return { connection: 'DatabaseConnectionInstance' };
});

// The first call creates the singleton instance
const conn1 = dbConnection;

// Subsequent calls reuse the existing instance
const conn2 = singleton('dbConnection', () => {
  console.log('This will not run because the instance already exists.');
  return { connection: 'AnotherDatabaseConnectionInstance' };
});

console.log(conn1 === conn2); // true

Output:

Creating database connection...
true

Why Use This Pattern?

  1. Performance:
    Creating and destroying resources like database connections repeatedly can be expensive. The singleton pattern minimizes this overhead by reusing instances.

  2. Global Accessibility:
    By storing singletons in the global object, they are accessible throughout the application without the need for complex dependency injection.

  3. Simplicity:
    This implementation is lightweight and requires minimal setup, making it ideal for scenarios like managing database connections or caching configuration objects.


Key Considerations

While this implementation is effective, there are a few important caveats to keep in mind:

1. Global Pollution

2. Memory Leaks

3. Type Safety


When is a Singleton Useful in Node.js?

The singleton pattern is particularly effective in the following scenarios:


Singleton in Cloud Function Context

If you’re working in a cloud function environment like AWS Lambda or Google Cloud Functions, the usefulness of a singleton depends on the execution model:

  1. Single-Request Handling (Default Setup):
    If each function instance handles only one request, singletons may have limited usefulness, as new instances of the function may be created for each invocation. However, singletons can still persist across "warm" invocations, improving performance.

  2. Concurrent Request Handling:
    If a function instance handles multiple requests concurrently, a singleton can efficiently manage shared resources, like a connection pool.

To ensure efficient use of resources in cloud functions, define your singleton outside the function handler:

const dbPool = singleton('dbPool', () => {
  console.log('Initializing connection pool...');
  return createConnectionPool(); // Example database pool creation
});

exports.handler = async (event) => {
  const connection = dbPool.getConnection();
  // Use the connection
};

Conclusion

The singleton function demonstrated above is a simple and practical way to manage shared resources in Node.js applications. Whether you’re building a backend API or a serverless function, the singleton pattern can help you optimize resource usage and improve application performance.

By keeping the implementation lightweight and mindful of global object usage, you can effectively leverage this pattern for a variety of use cases.