The Singleton Design Pattern in JavaScript: A Comprehensive Guide
Introduction
What is the Singleton Design Pattern?
The Singleton Design Pattern limits the instantiation of a class to only one instance. This is beneficial when a single object or shared resource is required to coordinate actions throughout the system. It’s especially handy for managing shared resources or data across various instances of a class. For instance, a database connection pool or a configuration manager might be implemented as a singleton to ensure consistent access and avoid resource conflicts.
The Need for Singleton Design Pattern
- Configuration Objects: Managing application settings across various components without having to create multiple instances of the configuration object.
- Database Connections: Keeping a single database connection pool that can be reused by different parts of the application, which is crucial for efficiency and scalability.
- Logging: Implementing a logging utility that centralizes the logging mechanism instead of creating multiple instances of the logger.
Two forms of singleton initialization:
-
Early Initialization – In this strategy, the singleton instance is created at the time of class loading. This means the instance is created and takes up resources even if it is never used during the application’s lifetime.
- Lazy Initialization – In this strategy,the singleton instance is created only when it is needed for the first time. This delays the creation of the instance until the point where it is accessed, thus saving resources if it is never used.
Lets see a basic example on how singleton works and then we will see how we implemented this pattern in a live project.
class Singleton {
constructor() {
if (Singleton.instance) {
// Singleton instance already exists.
return Singleton.instance;
}
Singleton.instance = this;
this.data = {};
return this;
}
getData(key) {
return this.data[key];
}
setData(key, value) {
this.data[key] = value;
}
}
const instance1 = new Singleton();
const instance2 = new Singleton();
instance1.setData('name', 'SingletonPattern');
console.log(instance2.getData('name')); // Output: SingletonPattern
console.log(instance1 === instance2); // Output: true
As shown above, we tried to create two different objects instance1
and instance2
which are strictly equal, which means both references are to the same instance, the output will be true
.
Now lets checkout a real example where we are using singleton class to create a database in a typescript project. We will have 2 files, db.ts for creating a database connection and app.ts, which will access database object.
//db.ts import { MongoClient } from 'mongodb'; class Database { private static instance: Database; private client: MongoClient; private connectionString: string = 'YOUR_MONGO_URI_CONNECTION_STRING'; private constructor() {} public static getInstance(): Database { console.log('Get Db Instance') if (!Database.instance) { console.log('Creating instance for the very first time') Database.instance = new Database(); } return Database.instance; } public async connect(): Promise<MongoClient> { console.log('connecting to db....') if (!this.client) { this.client = new MongoClient(this.connectionString); await this.client.connect(); console.log('Database connection established'); } return this.client; } public getClient(): MongoClient{ if( !this.client ){ this.client = new MongoClient(this.connectionString); await this.client.connect(); console.log('Database connection established'); } return this.client; } } export default Database;
// App.ts import Database from './db'; async function run() { try { const dbInstance = Database.getInstance(); const dbInstance2 = Database.getInstance(); // Lets create the database connection const client = await dbInstance.connect(); const database = client.db('production'); const collection = database.collection('partners'); // Perform database operations const documents = await collection.find().toArray(); console.log('Documents:', documents[0]); // lets check if both the clients from 2 instances are same or not? const client1 = dbInstance.getClient(); const client2 = dbInstance2.getClient(); console.log('Are both the clients same? -> ', client1===client2) // true } catch (error) { console.error('Error:', error); } }
In the above example, the getInstance()
method checks if the db
instance already exists and returns it if available; otherwise, it creates a new one. Whenever we fetch the Mongo client using getClient()
from any instance of the Database
class, it will always return the same client object created initially. This ensures efficient use of resources, consistency in database operations, and provides a centralized management point for the database connection.
Advantages:
- Controlled Access – The Singleton class has strict access over its sole instance on how and when it is being accessed.
- Resource Management – Singletons can help reduce resource consumption, such as memory and CPU usage, especially for heavy objects like database connections by ensuring only one instance is created,
- Global Access Point – The Singleton Pattern provides a single global access point to the instance, making it easy to access the instance from anywhere in the application.
- Consistency – By providing a single point of access to a resource, the Singleton pattern ensures that all parts of an application are aligned and consistent in their use of resources.
Disadvantages:
- Global State – This Pattern creates a global state in an application, which can lead to hidden dependencies between classes and modules, making the system harder to debug.
- Concurrency Issues – It can introduce concurrency issues in multi-threaded applications. When multiple threads try to instantiate the object at the same time, it can result in multiple objects instead of returning an instance. Therefore, developers must be privy to synchronization using thread locking or mutex in multi-threaded applications.
- Violates SRP – It tries to solve two tasks at the same time, ensuring that a class will have only one instance and assigning a global access point to the singleton class instance. Hence, violates single responsibility principle.
- Debugging Issues – Order of execution is important, when multiple components are updating the state, it might be difficult to trace the errors.
Conclusion
Singleton pattern provides a structure approach to manage shared resources and simplify access to globally required objects such as database connections, configuration settings, and logging utilities. It can introduce hidden dependencies, complicate testing, and potentially become a bottleneck in scalable, distributed systems. Therefore, it becomes important to evaluate the pros and cons judiciously and mitigate them through careful design and testing.