Skip to main content

Singleton Pattern đŸ¯

Definition: The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance.

đŸŽ¯ Intent​

Ensure a class has only one instance, and provide a global point of access to it. This is useful when exactly one object is needed to coordinate actions across the system.

🤔 Problem​

You need to ensure that a class has only one instance because:

  • Resource Management: Database connections, file systems, or print spoolers
  • Configuration Settings: Application settings that should be consistent
  • Logging: Central logging mechanism
  • Caching: Shared cache across the application
  • Thread Pools: Managing limited resources

Having multiple instances could cause:

  • Resource conflicts
  • Inconsistent behavior
  • Memory waste
  • Synchronization issues

💡 Solution​

The Singleton pattern solves this by making the class responsible for keeping track of its sole instance. The class ensures that no other instance can be created and provides a way to access the instance.

đŸ—ī¸ Structure​

Singleton
├── -instance: Singleton (static)
├── -constructor() (private)
├── +getInstance(): Singleton (static)
└── +businessLogic()

Client → Singleton.getInstance()

đŸ’ģ Simple Example​

Application Settings​

// Basic Singleton - Application Settings
class AppSettings {
constructor() {
// Prevent direct instantiation
if (AppSettings.instance) {
return AppSettings.instance;
}

// Initialize settings
this.settings = {
theme: 'light',
language: 'en',
apiUrl: 'https://api.example.com',
timeout: 5000,
debug: false
};

this.version = '1.0.0';
this.created = new Date();

// Store instance
AppSettings.instance = this;

console.log("âš™ī¸ AppSettings instance created");
}

static getInstance() {
if (!AppSettings.instance) {
console.log("🏭 Creating new AppSettings instance");
AppSettings.instance = new AppSettings();
} else {
console.log("â™ģī¸ Returning existing AppSettings instance");
}

return AppSettings.instance;
}

getSetting(key) {
console.log(`🔍 Getting setting: ${key} = ${this.settings[key]}`);
return this.settings[key];
}

setSetting(key, value) {
const oldValue = this.settings[key];
this.settings[key] = value;
console.log(`📝 Setting updated: ${key} = ${value} (was: ${oldValue})`);
}

getAllSettings() {
console.log("📋 Current Settings:");
Object.entries(this.settings).forEach(([key, value]) => {
console.log(` ${key}: ${value}`);
});
return { ...this.settings };
}

resetToDefaults() {
console.log("🔄 Resetting to default settings");
this.settings = {
theme: 'light',
language: 'en',
apiUrl: 'https://api.example.com',
timeout: 5000,
debug: false
};
}

getInfo() {
console.log(`â„šī¸ AppSettings Info:`);
console.log(` Version: ${this.version}`);
console.log(` Created: ${this.created.toLocaleString()}`);
console.log(` Instance ID: ${this.constructor.name}@${this.created.getTime()}`);
}
}

// Usage
console.log("=== Application Settings Singleton Demo ===\n");

console.log("Getting instances from different parts of the app:");
console.log("-".repeat(50));

// Simulate different modules accessing settings
console.log("1. Main App Module:");
const mainAppSettings = AppSettings.getInstance();
mainAppSettings.setSetting('theme', 'dark');
mainAppSettings.setSetting('debug', true);

console.log("\n2. User Interface Module:");
const uiSettings = AppSettings.getInstance();
console.log(`UI Theme: ${uiSettings.getSetting('theme')}`);

console.log("\n3. API Module:");
const apiSettings = AppSettings.getInstance();
console.log(`API URL: ${apiSettings.getSetting('apiUrl')}`);
apiSettings.setSetting('timeout', 10000);

console.log("\n4. Logger Module:");
const loggerSettings = AppSettings.getInstance();
console.log(`Debug Mode: ${loggerSettings.getSetting('debug')}`);

console.log("\nVerifying all instances are the same:");
console.log("-".repeat(35));
console.log(`mainAppSettings === uiSettings: ${mainAppSettings === uiSettings}`);
console.log(`uiSettings === apiSettings: ${uiSettings === apiSettings}`);
console.log(`apiSettings === loggerSettings: ${apiSettings === loggerSettings}`);

console.log("\nFinal settings state:");
console.log("-".repeat(20));
mainAppSettings.getAllSettings();
mainAppSettings.getInfo();

🌟 Real-World Example​

Database Connection Manager​

// Database Connection Singleton
class DatabaseManager {
constructor() {
if (DatabaseManager.instance) {
return DatabaseManager.instance;
}

this.connections = new Map();
this.connectionCount = 0;
this.maxConnections = 10;
this.activeQueries = 0;
this.totalQueries = 0;
this.created = new Date();

// Initialize default connection
this.initializeConnection('default', {
host: 'localhost',
port: 5432,
database: 'myapp',
username: 'admin',
password: '****'
});

DatabaseManager.instance = this;
console.log("đŸ—ƒī¸ DatabaseManager singleton created");
}

static getInstance() {
if (!DatabaseManager.instance) {
console.log("🏭 Creating new DatabaseManager instance");
DatabaseManager.instance = new DatabaseManager();
} else {
console.log("â™ģī¸ Returning existing DatabaseManager instance");
}

return DatabaseManager.instance;
}

initializeConnection(name, config) {
if (this.connections.has(name)) {
console.log(`âš ī¸ Connection '${name}' already exists`);
return false;
}

if (this.connectionCount >= this.maxConnections) {
console.log(`❌ Maximum connections reached (${this.maxConnections})`);
return false;
}

const connection = {
name,
config: { ...config, password: '****' }, // Hide password in logs
connected: false,
lastUsed: null,
queryCount: 0,
id: ++this.connectionCount
};

this.connections.set(name, connection);
console.log(`✅ Connection '${name}' initialized (ID: ${connection.id})`);
return true;
}

connect(connectionName = 'default') {
const connection = this.connections.get(connectionName);

if (!connection) {
console.log(`❌ Connection '${connectionName}' not found`);
return false;
}

if (connection.connected) {
console.log(`â„šī¸ Connection '${connectionName}' already connected`);
return true;
}

// Simulate connection establishment
console.log(`🔗 Connecting to database: ${connection.config.host}:${connection.config.port}/${connection.config.database}`);
connection.connected = true;
connection.lastUsed = new Date();

console.log(`✅ Connected to '${connectionName}'`);
return true;
}

query(sql, connectionName = 'default') {
const connection = this.connections.get(connectionName);

if (!connection) {
console.log(`❌ Connection '${connectionName}' not found`);
return null;
}

if (!connection.connected) {
console.log(`🔗 Auto-connecting to '${connectionName}'`);
if (!this.connect(connectionName)) {
return null;
}
}

// Simulate query execution
this.activeQueries++;
this.totalQueries++;
connection.queryCount++;
connection.lastUsed = new Date();

console.log(`📊 Executing query on '${connectionName}': ${sql}`);
console.log(` Active queries: ${this.activeQueries}`);

// Simulate query completion
setTimeout(() => {
this.activeQueries--;
}, 100);

return {
connection: connectionName,
query: sql,
result: `Result for: ${sql}`,
timestamp: new Date()
};
}

disconnect(connectionName = 'default') {
const connection = this.connections.get(connectionName);

if (!connection) {
console.log(`❌ Connection '${connectionName}' not found`);
return false;
}

if (!connection.connected) {
console.log(`â„šī¸ Connection '${connectionName}' already disconnected`);
return true;
}

connection.connected = false;
console.log(`🔒 Disconnected from '${connectionName}'`);
return true;
}

getConnectionInfo(connectionName = 'default') {
const connection = this.connections.get(connectionName);

if (!connection) {
console.log(`❌ Connection '${connectionName}' not found`);
return null;
}

console.log(`📋 Connection Info [${connectionName}]:`);
console.log(` ID: ${connection.id}`);
console.log(` Status: ${connection.connected ? 'Connected' : 'Disconnected'}`);
console.log(` Host: ${connection.config.host}:${connection.config.port}`);
console.log(` Database: ${connection.config.database}`);
console.log(` Queries: ${connection.queryCount}`);
console.log(` Last Used: ${connection.lastUsed ? connection.lastUsed.toLocaleString() : 'Never'}`);

return connection;
}

getStats() {
console.log(`📊 DatabaseManager Statistics:`);
console.log(` Total Connections: ${this.connections.size}/${this.maxConnections}`);
console.log(` Active Queries: ${this.activeQueries}`);
console.log(` Total Queries: ${this.totalQueries}`);
console.log(` Created: ${this.created.toLocaleString()}`);

const connectionStats = {};
this.connections.forEach((conn, name) => {
connectionStats[name] = {
connected: conn.connected,
queries: conn.queryCount
};
});

console.log(` Connection Status:`, connectionStats);
return connectionStats;
}

closeAllConnections() {
console.log("🔒 Closing all database connections");
let closedCount = 0;

this.connections.forEach((connection, name) => {
if (connection.connected) {
this.disconnect(name);
closedCount++;
}
});

console.log(`✅ Closed ${closedCount} connections`);
}
}

// Usage
console.log("\n=== Database Manager Singleton Demo ===\n");

console.log("Different modules accessing database:");
console.log("-".repeat(35));

// Module 1: User Service
console.log("1. User Service Module:");
const userServiceDB = DatabaseManager.getInstance();
userServiceDB.initializeConnection('users', {
host: 'user-db.company.com',
port: 5432,
database: 'users',
username: 'user_service'
});

userServiceDB.query("SELECT * FROM users WHERE active = true", 'users');

console.log("\n2. Product Service Module:");
const productServiceDB = DatabaseManager.getInstance();
productServiceDB.query("SELECT * FROM products WHERE in_stock = true");

console.log("\n3. Order Service Module:");
const orderServiceDB = DatabaseManager.getInstance();
orderServiceDB.initializeConnection('orders', {
host: 'order-db.company.com',
port: 5433,
database: 'orders',
username: 'order_service'
});

orderServiceDB.query("SELECT * FROM orders WHERE status = 'pending'", 'orders');

console.log("\nVerifying singleton behavior:");
console.log("-".repeat(30));
console.log(`userServiceDB === productServiceDB: ${userServiceDB === productServiceDB}`);
console.log(`productServiceDB === orderServiceDB: ${productServiceDB === orderServiceDB}`);

console.log("\nDatabase statistics:");
console.log("-".repeat(20));
userServiceDB.getStats();

console.log("\nConnection details:");
console.log("-".repeat(18));
userServiceDB.getConnectionInfo('default');
console.log();
userServiceDB.getConnectionInfo('users');
console.log();
userServiceDB.getConnectionInfo('orders');

console.log("\nShutdown:");
console.log("-".repeat(9));
userServiceDB.closeAllConnections();

🔧 Another Simple Example​

Logger Singleton​

// Logger Singleton with different log levels
class Logger {
constructor() {
if (Logger.instance) {
return Logger.instance;
}

this.logs = [];
this.level = 'info'; // debug, info, warn, error
this.maxLogs = 1000;
this.created = new Date();
this.logCount = 0;

Logger.instance = this;
console.log("📝 Logger singleton created");
}

static getInstance() {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}

setLevel(level) {
const levels = ['debug', 'info', 'warn', 'error'];
if (!levels.includes(level)) {
this.error(`Invalid log level: ${level}`);
return;
}

const oldLevel = this.level;
this.level = level;
this.info(`Log level changed from ${oldLevel} to ${level}`);
}

shouldLog(level) {
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
return levels[level] >= levels[this.level];
}

log(level, message, category = 'GENERAL') {
if (!this.shouldLog(level)) {
return;
}

const logEntry = {
id: ++this.logCount,
timestamp: new Date(),
level: level.toUpperCase(),
category,
message
};

this.logs.push(logEntry);

// Keep only recent logs
if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(-this.maxLogs);
}

// Output to console with formatting
const emoji = { debug: '🐛', info: 'â„šī¸', warn: 'âš ī¸', error: '❌' };
const timestamp = logEntry.timestamp.toLocaleTimeString();
console.log(`${emoji[level]} [${timestamp}] ${category}: ${message}`);
}

debug(message, category) {
this.log('debug', message, category);
}

info(message, category) {
this.log('info', message, category);
}

warn(message, category) {
this.log('warn', message, category);
}

error(message, category) {
this.log('error', message, category);
}

getStats() {
const stats = { debug: 0, info: 0, warn: 0, error: 0 };
this.logs.forEach(log => {
stats[log.level.toLowerCase()]++;
});

console.log("📊 Logger Statistics:");
console.log(` Total Logs: ${this.logCount}`);
console.log(` Current Level: ${this.level}`);
console.log(` Debug: ${stats.debug}`);
console.log(` Info: ${stats.info}`);
console.log(` Warn: ${stats.warn}`);
console.log(` Error: ${stats.error}`);
console.log(` Created: ${this.created.toLocaleString()}`);

return stats;
}

getRecentLogs(count = 10) {
const recent = this.logs.slice(-count);
console.log(`📋 Recent ${count} logs:`);
recent.forEach(log => {
console.log(` ${log.id}: [${log.level}] ${log.category} - ${log.message}`);
});
return recent;
}

clearLogs() {
const count = this.logs.length;
this.logs = [];
console.log(`đŸ—‘ī¸ Cleared ${count} logs`);
}
}

// Usage
console.log("\n=== Logger Singleton Demo ===\n");

console.log("Different modules using logger:");
console.log("-".repeat(30));

// Module 1
console.log("1. Authentication Module:");
const authLogger = Logger.getInstance();
authLogger.info("User login attempt", "AUTH");
authLogger.warn("Invalid password attempt", "AUTH");
authLogger.info("User successfully logged in", "AUTH");

console.log("\n2. Database Module:");
const dbLogger = Logger.getInstance();
dbLogger.debug("Connection pool initialized", "DATABASE");
dbLogger.info("Query executed successfully", "DATABASE");
dbLogger.error("Connection timeout", "DATABASE");

console.log("\n3. API Module:");
const apiLogger = Logger.getInstance();
apiLogger.info("API server started on port 3000", "API");
apiLogger.warn("Rate limit approaching for user 123", "API");

console.log("\nVerifying singleton:");
console.log("-".repeat(18));
console.log(`authLogger === dbLogger: ${authLogger === dbLogger}`);
console.log(`dbLogger === apiLogger: ${dbLogger === apiLogger}`);

console.log("\nChanging log level:");
console.log("-".repeat(18));
authLogger.setLevel('warn');

console.log("\nTesting with new level (debug should be filtered):");
authLogger.debug("This debug message won't show", "TEST");
authLogger.warn("This warning will show", "TEST");

console.log("\nLogger statistics:");
console.log("-".repeat(18));
authLogger.getStats();

console.log();
authLogger.getRecentLogs(5);

✅ Pros​

  • Controlled Access: Guarantees only one instance exists
  • Global Access: Provides a global point of access
  • Lazy Initialization: Instance created only when needed
  • Memory Efficient: Saves memory by avoiding multiple instances
  • Consistency: Ensures consistent state across the application

❌ Cons​

  • Global State: Can make testing and debugging difficult
  • Hidden Dependencies: Makes dependencies less explicit
  • Concurrency Issues: Can cause problems in multi-threaded environments
  • Tight Coupling: Can create tight coupling between components
  • Difficult to Mock: Hard to replace with mock objects for testing

đŸŽ¯ When to Use​

  • Resource Management: Database connections, file handles, thread pools
  • Configuration: Application settings that should be consistent
  • Logging: Central logging mechanism
  • Caching: Shared cache across the application
  • Hardware Access: Printer spoolers, device drivers
  • Coordinated Access: When you need to coordinate actions across the system

🔄 Implementation Variations​

1. Eager Initialization​

class EagerSingleton {
static instance = new EagerSingleton();

constructor() {
if (EagerSingleton.instance) {
return EagerSingleton.instance;
}
}

static getInstance() {
return EagerSingleton.instance;
}
}

2. Lazy Initialization (shown in examples above)​

static getInstance() {
if (!MySingleton.instance) {
MySingleton.instance = new MySingleton();
}
return MySingleton.instance;
}

3. Thread-Safe (for Node.js with worker threads)​

const singletonInstances = new Map();

class ThreadSafeSingleton {
constructor() {
const className = this.constructor.name;
if (singletonInstances.has(className)) {
return singletonInstances.get(className);
}
singletonInstances.set(className, this);
}
}

âš ī¸ Anti-Patterns to Avoid​

  • Using Singleton for everything: Not every class needs to be a singleton
  • Making everything global: Reduces testability and maintainability
  • Ignoring dependency injection: Singleton can hide dependencies
  • Not considering alternatives: Sometimes simple module patterns work better
  • Factory Method: Can use Singleton to ensure only one factory exists
  • Abstract Factory: Factory instances are often singletons
  • Builder: Director object might be a singleton
  • Prototype: Prototype registry can be implemented as singleton
  • Facade: Facade objects are often implemented as singletons

📚 Further Reading​