What is the Singleton Design Pattern
The Singleton Design Pattern states that a class can only be instantiated once and then that instance is shared globally throughout the application.
When to use the Singleton Design Pattern
- Global Configuration Management โ Store app-wide settings or environment variables in a single instance.
- Logging Services โ Ensure all parts of your application write logs to the same instance to avoid duplicate loggers.
- Database Connections โ Prevent multiple connections to the database, reducing resource consumption and improving performance.
- Caching Data โ Maintain a single source of truth for frequently accessed data, improving efficiency.
- Authentication & User Sessions โ Manage user session data in a centralized instance to keep track of logged-in users.
Examples
โ Bad Example: Violating the Singleton Pattern
Here's an example that violates the singleton design pattern
class ConfigManager {
constructor() {
this.settings = {}
}
set(key, value) {
this.settings[key] = value
}
get(key) {
return this.settings[key]
}
}
const config1 = new ConfigManager()
const config2 = new ConfigManager()
Above, we have a ConfigManager class that allows us to set different configuration properties on the settings object using the set and get methods.
The problem is that the class can be instantiated more than once.
We have two objects config1 and config2. If we set a theme property on both objects and log them, we will get different results.
config1.set("theme", "dark")
config2.set("theme", "light")
console.log(config1.get("theme"))
console.log(config2.get("theme"))
console.log(config1 === config2)
As you can see, the theme we get is different and also on comparing both objects we get false as they are different instances.
โ Correct Example: Implementing the Singleton Pattern
Now let's fix the class we created above and make it follow the singleton pattern.
let instance = null
class ConfigManager {
constructor() {
if (instance) return instance
this.settings = {}
instance = this
return instance
}
set(key, value) {
this.settings[key] = value
}
get(key) {
return this.settings[key]
}
}
const config1 = new ConfigManager()
const config2 = new ConfigManager()
Now this class is following the singleton pattern as we can only create a singleton instance. Let's try to understand what we did above:
let instance = null
We created a variable to keep track of the instance, so we can restrict the user from creating the instance twice.
constructor() {
if (instance) return instance
this.settings = {}
instance = this
return instance
}
Then inside the constructor we check if the instance already exist, we return the already created instance instead of creating a new one. Otherwise we create a new instance, store it in the variable and return it.
config1.set("theme", "dark")
config2.set("theme", "light")
console.log(config1.get("theme"))
console.log(config2.get("theme"))
console.log(config1 === config2)
As you can see, we set a theme of dark on config1 and light on config2 but when we log them, we get the same result, also on comparing the objects we get true. This is because both config1 and config2 are the same instances.
Pros and Cons of the Singleton Design Pattern
โ Pros
- Ensures a Single Instance โ Prevents multiple instances, making it ideal for managing shared resources (e.g., logging, database connections, configurations).
- Global Access โ Provides a centralized and easy way to access the instance from anywhere in the application.
- Saves Memory โ Reduces memory usage by avoiding redundant object creation.
โ Cons
- Hidden Dependencies โ Since it's a global instance, different parts of the app may depend on it, making debugging and testing harder.
- Makes Unit Testing Difficult โ Singletons introduce global state, which can lead to side effects and make unit testing challenging.
Conclusion
The Singleton Design Pattern is useful when you need to enforce a single shared instance across your application.
However, it can also introduce hidden dependencies and testing challenges if not used carefully. Always consider whether a global instance is truly necessary before implementing this pattern.
By understanding its proper use cases, you can leverage Singleton to write cleaner, more efficient JavaScript code while avoiding potential pitfalls.