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:

    • The function uses the Node.js global object to store singleton instances.
    • A property named __singletons is added to the global object to act as a container for all singleton instances.
  2. Lazy Initialization:

    • If the requested singleton doesn’t exist in the __singletons object, the value factory function is called to create it. Otherwise, the existing instance is returned.
  3. Reusable Across Calls:

    • Once created, the singleton instance is reused for all subsequent requests with the same name.

Function Parameters

  • name: string
    A unique identifier for the singleton instance. Each singleton is stored and accessed using this name.

  • value: () => Value
    A factory function that generates the value. This is invoked only once, the first time the singleton is requested.


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
  • The factory function (value) runs only during the first call, creating the singleton instance.
  • Further calls with the same name return the already-created instance.

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

  • Adding properties to the global object can lead to conflicts. Ensure the property name (__singletons in this case) is unique and unlikely to collide with other global properties.

2. Memory Leaks

  • Objects stored in the global object persist for the lifetime of the application. Be cautious about adding large or temporary objects to the singleton store.

3. Type Safety

  • The use of globalAny circumvents TypeScript’s type system, potentially leading to runtime errors. You can improve type safety by creating a typed interface for the global object.

When is a Singleton Useful in Node.js?

The singleton pattern is particularly effective in the following scenarios:

  • Database Connections: Maintain a single database connection pool across multiple modules.
  • Configuration Objects: Share application-wide configuration settings.
  • Caching: Reuse expensive calculations or API responses.

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.