← Back to principles

Design Principle

Singleton Pattern

Learn the Singleton pattern: ensure a class has only one instance and provide global access to it. Understand when to use it and common pitfalls.

The Singleton Pattern ensures a class has only one instance and provides a global point of access to that instance. It's useful when exactly one object is needed to coordinate actions across the system.


What is the Singleton Pattern?

Singleton Pattern guarantees:

  • Single instance: Only one instance of the class exists
  • Global access: Provides a way to access that instance
  • Lazy initialization: Instance created only when needed
  • Controlled access: Prevents creation of multiple instances

Use cases:

  • Database connections
  • Logger instances
  • Configuration managers
  • Cache managers
  • Thread pools

Implementation

Basic Singleton (Not Thread-Safe)

class Singleton {
  private static instance: Singleton;
  
  // Private constructor prevents instantiation
  private constructor() {}
  
  // Public method to get instance
  public static getInstance(): Singleton {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

// Usage
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true

Thread-Safe Singleton (Eager Initialization)

class Singleton {
  // Eager initialization - created at class load time
  private static instance: Singleton = new Singleton();
  
  private constructor() {}
  
  public static getInstance(): Singleton {
    return Singleton.instance;
  }
}

Thread-Safe Singleton (Lazy Initialization with Double-Check Locking)

class Singleton {
  private static instance: Singleton;
  private static lock: object = new Object();
  
  private constructor() {}
  
  public static getInstance(): Singleton {
    if (!Singleton.instance) {
      synchronized (Singleton.lock) {
        if (!Singleton.instance) {
          Singleton.instance = new Singleton();
        }
      }
    }
    return Singleton.instance;
  }
}

Examples

Database Connection Singleton

class DatabaseConnection {
  private static instance: DatabaseConnection;
  private connection: any;
  
  private constructor() {
    // Initialize database connection
    this.connection = this.initializeConnection();
  }
  
  public static getInstance(): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection();
    }
    return DatabaseConnection.instance;
  }
  
  private initializeConnection(): any {
    // Database connection logic
    return { /* connection object */ };
  }
  
  public getConnection(): any {
    return this.connection;
  }
}

// Usage
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
// db1 and db2 are the same instance

Logger Singleton

class Logger {
  private static instance: Logger;
  private logs: string[] = [];
  
  private constructor() {}
  
  public static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }
  
  public log(message: string): void {
    const timestamp = new Date().toISOString();
    this.logs.push(`[${timestamp}] ${message}`);
    console.log(`[${timestamp}] ${message}`);
  }
  
  public getLogs(): string[] {
    return [...this.logs];
  }
}

// Usage
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
logger1.log("Application started");
logger2.log("User logged in");
// Both log to the same instance

Common Pitfalls

  • Thread safety: Not thread-safe in multi-threaded environments. Fix: Use double-check locking or eager initialization
  • Testing difficulty: Hard to test due to global state. Fix: Use dependency injection, make singleton testable
  • Hidden dependencies: Creates hidden dependencies. Fix: Use dependency injection instead
  • Global state: Can lead to global state issues. Fix: Consider alternatives like dependency injection
  • Lazy initialization issues: Can cause issues in multi-threaded environments. Fix: Use proper synchronization

Interview Questions

Beginner

Q: What is the Singleton pattern and when would you use it?

A:

Singleton Pattern ensures a class has only one instance and provides global access to it.

Key characteristics:

  • Single instance: Only one instance exists
  • Private constructor: Prevents direct instantiation
  • Static method: Provides access to the instance
  • Global access: Can be accessed from anywhere

When to use:

  • Database connections: Single connection pool
  • Loggers: Single logging instance
  • Configuration: Single configuration manager
  • Cache: Single cache instance
  • Thread pools: Single thread pool manager

Example:

class Singleton {
  private static instance: Singleton;
  
  private constructor() {}
  
  public static getInstance(): Singleton {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

Benefits:

  • Controlled access: Only one instance
  • Global access: Accessible from anywhere
  • Lazy initialization: Created when needed

Intermediate

Q: Explain thread-safety issues with the Singleton pattern. How do you implement a thread-safe Singleton?

A:

Thread-Safety Issues:

Problem:

// Not thread-safe
public static getInstance(): Singleton {
  if (!Singleton.instance) {  // Thread 1 checks
    // Thread 2 might also check here
    Singleton.instance = new Singleton();  // Both create instances
  }
  return Singleton.instance;
}

Multiple threads can create multiple instances.

Solutions:

1. Eager Initialization:

// Created at class load time (thread-safe)
private static instance: Singleton = new Singleton();

public static getInstance(): Singleton {
  return Singleton.instance;
}

2. Double-Check Locking:

private static instance: Singleton;
private static lock: object = new Object();

public static getInstance(): Singleton {
  if (!Singleton.instance) {
    synchronized (lock) {
      if (!Singleton.instance) {
        instance = new Singleton();
      }
    }
  }
  return Singleton.instance;
}

3. Enum Singleton (Java):

public enum Singleton {
  INSTANCE;
  
  public void doSomething() {
    // ...
  }
}

Trade-offs:

  • Eager: Simple, but instance created even if not used
  • Double-check: Lazy, but more complex
  • Enum: Thread-safe, but less flexible

Senior

Q: Design a thread-safe Singleton pattern for a high-concurrency application. How do you handle lazy initialization, prevent multiple instances, and ensure performance?

A:

class HighPerformanceSingleton {
  private static instance: HighPerformanceSingleton;
  private static readonly lock: object = new Object();
  private static initialized: boolean = false;
  
  private constructor() {
    // Expensive initialization
    this.initialize();
  }
  
  // Thread-safe lazy initialization
  public static getInstance(): HighPerformanceSingleton {
    // First check (no lock) - performance optimization
    if (!HighPerformanceSingleton.initialized) {
      synchronized (HighPerformanceSingleton.lock) {
        // Second check (with lock) - prevent double initialization
        if (!HighPerformanceSingleton.initialized) {
          HighPerformanceSingleton.instance = new HighPerformanceSingleton();
          HighPerformanceSingleton.initialized = true;
        }
      }
    }
    return HighPerformanceSingleton.instance;
  }
  
  private initialize(): void {
    // Expensive initialization logic
  }
}

// Alternative: Using volatile (Java/C#)
class VolatileSingleton {
  private static volatile VolatileSingleton instance;
  private static readonly lock: object = new Object();
  
  private constructor() {}
  
  public static getInstance(): VolatileSingleton {
    if (instance == null) {
      synchronized (lock) {
        if (instance == null) {
          instance = new VolatileSingleton();
        }
      }
    }
    return instance;
  }
}

// Modern approach: Using initialization-on-demand holder (Java)
class HolderSingleton {
  private HolderSingleton() {}
  
  private static class Holder {
    private static final HolderSingleton INSTANCE = new HolderSingleton();
  }
  
  public static HolderSingleton getInstance() {
    return Holder.INSTANCE;
  }
}

Features:

  1. Double-check locking: Prevents multiple instances
  2. Volatile: Ensures visibility across threads
  3. Lazy initialization: Created only when needed
  4. Performance: First check without lock for speed

Key Takeaways

  • Singleton pattern: Ensures only one instance exists
  • Private constructor: Prevents direct instantiation
  • Static method: Provides access to instance
  • Thread safety: Use double-check locking or eager initialization
  • Use cases: Database connections, loggers, configuration managers
  • Pitfalls: Testing difficulty, hidden dependencies, global state
  • Best practices: Consider dependency injection, make testable, use proper synchronization

Keep exploring

Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.