Learning Objectives

  • Understand the Singleton pattern concept
  • Implement Singleton with classes and modules
  • Use Singleton for configuration and state management
  • Recognize when to use (and avoid) Singletons
  • Apply best practices and avoid pitfalls

What is the Singleton Pattern?

A Singleton restricts instantiation of a class to a single object. No matter how many times you try to create an instance, you always get the same one.

Basic Implementation

class Database {
    constructor() {
        if (Database.instance) {
            return Database.instance;
        }
        
        this.connection = null;
        Database.instance = this;
    }
    
    connect(url) {
        if (!this.connection) {
            this.connection = { url, connected: true };
            console.log(`Connected to ${url}`);
        }
    }
    
    query(sql) {
        if (!this.connection) {
            throw new Error('Not connected');
        }
        return `Executing: ${sql}`;
    }
}

// Always returns the same instance
const db1 = new Database();
const db2 = new Database();

console.log(db1 === db2); // true
db1.connect('mongodb://localhost');
console.log(db2.connection); // Same connection!

ES6 Module Singleton

The simplest way - export a single instance:

// database.js
class Database {
    constructor() {
        this.connection = null;
    }
    
    connect(url) {
        this.connection = { url, connected: true };
    }
    
    query(sql) {
        return `Executing: ${sql}`;
    }
}

export default new Database();
// app.js
import db from './database.js';

db.connect('mongodb://localhost');
const result = db.query('SELECT * FROM users');

Every import gets the same instance!

Real-World Examples

Configuration Singleton

class Config {
    constructor() {
        if (Config.instance) {
            return Config.instance;
        }
        
        this.settings = {
            apiUrl: process.env.API_URL || 'https://api.example.com',
            timeout: 5000,
            debug: process.env.NODE_ENV === 'development'
        };
        
        Config.instance = this;
    }
    
    get(key) {
        return this.settings[key];
    }
    
    set(key, value) {
        this.settings[key] = value;
    }
    
    getAll() {
        return { ...this.settings };
    }
}

const config = new Config();
export default config;

Logger Singleton

class Logger {
    constructor() {
        if (Logger.instance) {
            return Logger.instance;
        }
        
        this.logs = [];
        Logger.instance = this;
    }
    
    log(message) {
        const entry = {
            message,
            timestamp: new Date(),
            level: 'INFO'
        };
        this.logs.push(entry);
        console.log(`[${entry.timestamp.toISOString()}] ${message}`);
    }
    
    error(message) {
        const entry = {
            message,
            timestamp: new Date(),
            level: 'ERROR'
        };
        this.logs.push(entry);
        console.error(`[${entry.timestamp.toISOString()}] ERROR: ${message}`);
    }
    
    warn(message) {
        const entry = {
            message,
            timestamp: new Date(),
            level: 'WARN'
        };
        this.logs.push(entry);
        console.warn(`[${entry.timestamp.toISOString()}] WARN: ${message}`);
    }
    
    getLogs(level = null) {
        if (level) {
            return this.logs.filter(log => log.level === level);
        }
        return [...this.logs];
    }
    
    clear() {
        this.logs = [];
    }
}

export default new Logger();

State Management Singleton

class Store {
    constructor() {
        if (Store.instance) {
            return Store.instance;
        }
        
        this.state = {};
        this.listeners = [];
        Store.instance = this;
    }
    
    getState() {
        return { ...this.state };
    }
    
    setState(newState) {
        this.state = { ...this.state, ...newState };
        this.notify();
    }
    
    subscribe(listener) {
        this.listeners.push(listener);
        
        // Return unsubscribe function
        return () => {
            this.listeners = this.listeners.filter(l => l !== listener);
        };
    }
    
    notify() {
        this.listeners.forEach(listener => listener(this.state));
    }
}

const store = new Store();
export default store;
// Usage
import store from './store.js';

// Subscribe to state changes
const unsubscribe = store.subscribe(state => {
    console.log('State changed:', state);
});

// Update state
store.setState({ user: { name: 'John' } });
store.setState({ count: 0 });

// Unsubscribe
unsubscribe();

Lazy Initialization

Create instance only when needed:

class HeavyResource {
    constructor() {
        console.log('Initializing heavy resource...');
        this.data = this.loadHeavyData();
    }
    
    loadHeavyData() {
        // Expensive operation
        const data = new Array(1000000).fill(0).map((_, i) => ({
            id: i,
            value: Math.random()
        }));
        return data;
    }
    
    static getInstance() {
        if (!HeavyResource.instance) {
            HeavyResource.instance = new HeavyResource();
        }
        return HeavyResource.instance;
    }
    
    getData() {
        return this.data;
    }
}

// Not created until first use
console.log('App started');
// ... other code ...
const resource = HeavyResource.getInstance(); // Created here

When to Use Singletons

Good use cases:
  • Configuration management
  • Logging systems
  • Database connections
  • Caching mechanisms
  • Thread pools
  • Device drivers
Bad use cases:
  • When you need multiple instances
  • Testing scenarios (hard to mock)
  • When state should be isolated
  • When it creates tight coupling

Pros and Cons

Pros

Cons

Testing Singletons

class TestableDatabase {
    constructor() {
        if (TestableDatabase.instance && !TestableDatabase.testing) {
            return TestableDatabase.instance;
        }
        
        this.connection = null;
        if (!TestableDatabase.testing) {
            TestableDatabase.instance = this;
        }
    }
    
    static reset() {
        TestableDatabase.instance = null;
    }
    
    static enableTesting() {
        TestableDatabase.testing = true;
    }
    
    static disableTesting() {
        TestableDatabase.testing = false;
    }
}

// In tests
describe('Database', () => {
    beforeEach(() => {
        TestableDatabase.enableTesting();
        TestableDatabase.reset();
    });
    
    afterEach(() => {
        TestableDatabase.disableTesting();
    });
    
    it('should create new instance in test mode', () => {
        const db1 = new TestableDatabase();
        const db2 = new TestableDatabase();
        expect(db1).not.toBe(db2);
    });
});

Alternatives to Singleton

Dependency Injection

class UserService {
    constructor(database, logger) {
        this.db = database;
        this.logger = logger;
    }
    
    async getUser(id) {
        this.logger.log(`Fetching user ${id}`);
        return await this.db.query(`SELECT * FROM users WHERE id = ${id}`);
    }
}

// Inject dependencies
const db = new Database();
const logger = new Logger();
const userService = new UserService(db, logger);

Factory Pattern

class DatabaseFactory {
    static instances = new Map();
    
    static create(name) {
        if (!this.instances.has(name)) {
            this.instances.set(name, new Database());
        }
        return this.instances.get(name);
    }
}

const mainDb = DatabaseFactory.create('main');
const cacheDb = DatabaseFactory.create('cache');

Best Practices

1. Use ES6 modules for simple singletons
// config.js
export default {
    apiUrl: 'https://api.example.com',
    timeout: 5000
};
2. Make it clear it's a singleton
class DatabaseSingleton {
    // Clear naming convention
}
3. Provide a way to reset for testing
class Logger {
    static reset() {
        Logger.instance = null;
    }
}
4. Consider lazy initialization
static getInstance() {
    if (!this.instance) {
        this.instance = new this();
    }
    return this.instance;
}
5. Document singleton behavior
/**
 * Database connection manager (Singleton)
 * Only one instance exists throughout the application
 */
class Database {
    // ...
}

Key Takeaways