Singleton Pattern
The Singleton pattern ensures that a class has only one instance and provides a global access point to that instance. It's useful for managing shared resources like database connections, configuration settings, or logging services.
Key Characteristics
- Single Instance - Only one object of the class exists
- Global Access - The instance is accessible from anywhere in the application
- Lazy Initialization - Instance is created only when first needed
- Controlled Access - Constructor or factory method controls instantiation
Interactive Demo
Try creating multiple instances and see how they all reference the same object:
Output
// Click buttons to interact with the Singleton
Instances Created: 0
Total Logs: 0
Implementation Examples
Basic Singleton
// Basic Singleton Pattern
class DatabaseConnection {
constructor() {
if (DatabaseConnection.instance) {
return DatabaseConnection.instance;
}
this.connected = false;
this.queries = [];
DatabaseConnection.instance = this;
}
connect() {
if (!this.connected) {
this.connected = true;
console.log('Database connected');
}
}
query(sql) {
if (this.connected) {
this.queries.push(sql);
console.log(`Executing: ${sql}`);
}
}
getQueryCount() {
return this.queries.length;
}
}
// Usage
const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();
console.log(db1 === db2); // true - same instance! Modern Singleton with Configuration
// Modern Singleton with Module Pattern
class ConfigurationManager {
constructor() {
if (ConfigurationManager.instance) {
return ConfigurationManager.instance;
}
this.config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
ConfigurationManager.instance = this;
}
get(key) {
return this.config[key];
}
set(key, value) {
this.config[key] = value;
}
getAll() {
return { ...this.config };
}
}
// ES6 Module Singleton (even simpler)
export const config = new ConfigurationManager(); Lazy Initialization with Static Method
// Lazy Initialization Singleton
class Logger {
static #instance = null;
constructor() {
if (Logger.#instance) {
throw new Error('Use Logger.getInstance()');
}
this.logs = [];
}
static getInstance() {
if (!Logger.#instance) {
Logger.#instance = new Logger();
}
return Logger.#instance;
}
log(message) {
const entry = {
timestamp: new Date().toISOString(),
message
};
this.logs.push(entry);
console.log(`[${entry.timestamp}] ${message}`);
}
getLogs() {
return this.logs;
}
clear() {
this.logs = [];
}
}
// Usage
const logger = Logger.getInstance();
logger.log('Application started'); Common Use Cases
- Database Connections - Share a single connection pool
- Configuration Management - Single source of truth for app settings
- Logging Services - Centralized logging to one destination
- Cache Managers - Shared cache across the application
- Thread Pools - Manage a limited set of worker threads
Benefits
- Controlled Access - Only one instance ensures consistency
- Resource Management - Prevents duplicate resource allocation
- Global State - Easy access to shared state
- Lazy Loading - Instance created only when needed
Trade-offs and Concerns
- Global State - Can make testing difficult and hide dependencies
- Tight Coupling - Code becomes coupled to the singleton instance
- Concurrency Issues - Requires careful handling in multi-threaded environments
- Violates SRP - Class manages both its functionality and instance control
- Hidden Dependencies - Not clear from function signatures what depends on singleton
Alternatives to Consider
- Dependency Injection - Pass instances explicitly rather than using global state
- ES6 Modules - Module exports are singletons by default
- Factory Functions - Control instance creation without enforcing single instance
- Context Objects - Pass context through your application explicitly