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

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
Global Object as Storage:
- The function uses the Node.js
globalobject to store singleton instances. - A property named
__singletonsis added to theglobalobject to act as a container for all singleton instances.
- The function uses the Node.js
Lazy Initialization:
- If the requested singleton doesn’t exist in the
__singletonsobject, thevaluefactory function is called to create it. Otherwise, the existing instance is returned.
- If the requested singleton doesn’t exist in the
Reusable Across Calls:
- Once created, the singleton instance is reused for all subsequent requests with the same
name.
- Once created, the singleton instance is reused for all subsequent requests with the same
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
namereturn the already-created instance.
Why Use This Pattern?
Performance:
Creating and destroying resources like database connections repeatedly can be expensive. The singleton pattern minimizes this overhead by reusing instances.Global Accessibility:
By storing singletons in theglobalobject, they are accessible throughout the application without the need for complex dependency injection.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
globalobject can lead to conflicts. Ensure the property name (__singletonsin this case) is unique and unlikely to collide with other global properties.
2. Memory Leaks
- Objects stored in the
globalobject 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
globalAnycircumvents TypeScript’s type system, potentially leading to runtime errors. You can improve type safety by creating a typed interface for theglobalobject.
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:
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.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.