Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

README.md

Singleton Pattern 👤

Overview

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

Difficulty: ⭐ Easy
Category: Creational Pattern
Interview Frequency: ⭐⭐⭐⭐⭐ EXTREMELY COMMON

Problem It Solves

Sometimes you need exactly one instance of a class:

  • Database connections (one connection pool)
  • Logger (single logging instance)
  • Configuration manager (one config object)
  • Thread pools (one executor service)

Without Singleton, you'd create multiple instances, wasting resources and causing inconsistency.

Real-World Analogy

Think of a government:

  • Each country has ONE president (at a time)
  • Trying to create a new one fails (only one can be in office)
  • Everyone accesses the same president
  • This is a Singleton pattern in action

When to Use

Use Singleton Pattern when:

  • You need exactly one instance of a class
  • Instances are expensive to create
  • You need global access (be careful!)
  • Thread safety is required
  • You need lazy initialization

Don't use when:

  • You might need multiple instances later
  • It's better to use dependency injection
  • Testing becomes harder (global state)
  • You have thread safety concerns (not addressed)

Implementation Approaches

1. Eager Initialization (Thread-safe by default)

class Database {
  private static instance = new Database();

  private constructor() {}

  static getInstance(): Database {
    return Database.instance;
  }
}

2. Lazy Initialization (Requires synchronization)

class Database {
  private static instance?: Database;

  private constructor() {}

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

3. Double-Checked Locking (Complex)

class Database {
  private static instance?: Database;

  private constructor() {}

  static getInstance(): Database {
    if (!Database.instance) {
      // Second check inside lock
      if (!Database.instance) {
        Database.instance = new Database();
      }
    }
    return Database.instance;
  }
}

TypeScript-Specific Implementation

// Modern TypeScript approach using ?? (nullish coalescing)
class Logger {
  private static instance?: Logger;

  private constructor() {}

  static getInstance(): Logger {
    return (this.instance ??= new Logger());
  }
}

// Usage
const logger = Logger.getInstance();
const sameLogger = Logger.getInstance();
console.log(logger === sameLogger); // true

Benefits

Single Instance: Guaranteed one and only one
Global Access: Available everywhere via getInstance()
Lazy Initialization: Created only when first needed
Controlled: Class controls its own instantiation
Thread-Safe (with proper implementation)

Drawbacks

Global State: Makes testing harder
Hidden Dependencies: Hard to see what depends on what
Tight Coupling: Hard to replace with mock in tests
Violates SRP: Object creation + business logic mixed
Thread Safety Issues: Tricky to implement correctly
Performance: Synchronization overhead

Interview Questions & Answers

Q1: Is Singleton thread-safe in your implementation?

Answer: Depends on approach:

Eager (Thread-safe):

class Database {
  private static instance = new Database(); // Created at class load time
  private constructor() {}
  static getInstance(): Database {
    return Database.instance;
  }
}
// Thread-safe because instance is created before any thread can call getInstance()

Lazy (NOT thread-safe):

class Database {
  private static instance?: Database;
  static getInstance(): Database {
    if (!Database.instance) {
      // RACE CONDITION: Two threads could create instances!
      Database.instance = new Database();
    }
    return Database.instance;
  }
}

Lazy + Lock (Thread-safe):

class Database {
  private static instance?: Database;
  private static lock = new Mutex();

  static async getInstance(): Promise<Database> {
    if (!Database.instance) {
      await Database.lock.lock();
      try {
        if (!Database.instance) {
          Database.instance = new Database();
        }
      } finally {
        Database.lock.unlock();
      }
    }
    return Database.instance;
  }
}

Q2: What's the difference between Singleton and Static Class?

Answer:

Aspect Singleton Static Class
Instance One instance No instances
State Maintains instance state Only static state
Interface Can implement interfaces Cannot implement interfaces
Inheritance Can extend classes Limited inheritance
Testing Can mock (with difficulty) Hard to mock
Memory Uses memory for instance No instance memory

Singleton:

class Logger {
  private logs: string[] = [];

  log(msg: string) {
    this.logs.push(msg);
  }

  static getInstance(): Logger {
    // ...
  }
}

Static Class:

class Logger {
  private static logs: string[] = [];

  static log(msg: string) {
    Logger.logs.push(msg);
  }
}

Q3: How do you test a Singleton?

Answer: Challenges and solutions:

Challenge: Singletons are global, making unit tests interdependent.

Solution 1: Reset in tests:

class Logger {
  private static instance?: Logger;

  static getInstance(): Logger {
    return (this.instance ??= new Logger());
  }

  // Add reset method for testing
  static reset(): void {
    this.instance = undefined;
  }
}

// Test
beforeEach(() => {
  Logger.reset(); // Fresh instance for each test
});

Solution 2: Use dependency injection instead:

// Better: Don't use Singleton!
class Service {
  constructor(private logger: Logger) {}
}

// Test
const mockLogger = new MockLogger();
const service = new Service(mockLogger);

Q4: Can you break a Singleton pattern?

Answer: Yes! Several ways:

1. Reflection (in languages that support it):

// Not possible in TypeScript without special tricks

2. Cloning:

const singleton = Logger.getInstance();
const clone = Object.create(singleton); // Creates a copy!

3. Serialization:

// Deserializing can create a new instance

4. Multiple threads (without synchronization):

// As discussed above - race condition

Q5: Singleton vs Dependency Injection—which is better?

Answer: Dependency Injection is better!

Singleton (Anti-pattern):

// Hard to test, tight coupling
class UserService {
  private db = Database.getInstance(); // Global dependency

  getUser(id: string) {
    return this.db.query(...);
  }
}

// Test is hard - you get the real database!
const service = new UserService();
service.getUser("1"); // Uses real DB, not mock

Dependency Injection (Best practice):

// Easy to test, loose coupling
class UserService {
  constructor(private db: Database) {} // Injected

  getUser(id: string) {
    return this.db.query(...);
  }
}

// Test with mock
const mockDb = new MockDatabase();
const service = new UserService(mockDb);
service.getUser("1"); // Uses mock!

Use Singleton for: Cross-cutting concerns you absolutely need (rarely)
Use DI for: Everything else (preferred)

Q6: What about the "Bill Pugh Singleton"?

Answer: This is Java-specific using class loaders. In TypeScript:

// TypeScript equivalent using module pattern
const DatabaseSingleton = (() => {
  let instance: Database;

  return {
    getInstance(): Database {
      if (!instance) {
        instance = new Database();
      }
      return instance;
    },
  };
})();

// Usage
const db1 = DatabaseSingleton.getInstance();
const db2 = DatabaseSingleton.getInstance();
console.log(db1 === db2); // true

This is thread-safe in JavaScript/TypeScript because module loading is synchronized.

Real-World Use Cases

  1. Logging: Single logger instance for entire app
  2. Database Connections: Connection pool singleton
  3. Configuration: Single config object
  4. Thread Pools: Single executor service
  5. Caches: Single cache instance
  6. Event Bus: Single global event emitter
  7. Application Settings: One settings object

Code Smell Fixes

Before (Multiple instances):

class Logger {
  log(msg: string): void {
    console.log(`[${new Date().toISOString()}] ${msg}`);
  }
}

// Problem: Multiple instances everywhere!
const logger1 = new Logger();
const logger2 = new Logger();

logger1.log("Event 1"); // Goes to console1
logger2.log("Event 2"); // Goes to console2
// Logs are split between instances!

After (Single instance):

class Logger {
  private static instance?: Logger;

  private constructor() {}

  static getInstance(): Logger {
    return (this.instance ??= new Logger());
  }

  log(msg: string): void {
    console.log(`[${new Date().toISOString()}] ${msg}`);
  }
}

// Only one instance
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();

logger1.log("Event 1"); // Same instance
logger2.log("Event 2"); // Same instance
// All logs go to same place!

Common Pitfalls

Public constructor - Anyone can create instances

class Logger {
  // BAD - can create multiple instances
  constructor() {}
}

Not thread-safe - Race conditions

class Logger {
  private static instance?: Logger;

  // BAD - race condition!
  static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }
}

Hard to test - Can't inject mocks

class UserService {
  // BAD - hard to test because tied to real Logger
  private logger = Logger.getInstance();
}

Global state - Breaks encapsulation

// BAD - everyone depends on global Logger
const logger = Logger.getInstance();

Testing Singleton

describe("Logger Singleton", () => {
  beforeEach(() => {
    Logger.reset(); // Reset for each test
  });

  test("should return same instance", () => {
    const logger1 = Logger.getInstance();
    const logger2 = Logger.getInstance();
    expect(logger1).toBe(logger2);
  });

  test("should initialize once", () => {
    expect(Logger.getInstanceCount()).toBe(0);
    const logger = Logger.getInstance();
    expect(Logger.getInstanceCount()).toBe(1);
    const logger2 = Logger.getInstance();
    expect(Logger.getInstanceCount()).toBe(1); // Still 1!
  });
});

Variations

1. Multiton (Named Singleton)

class Logger {
  private static instances = new Map<string, Logger>();

  static getInstance(name: string): Logger {
    if (!this.instances.has(name)) {
      this.instances.set(name, new Logger(name));
    }
    return this.instances.get(name)!;
  }
}

// Multiple named singletons
const appLogger = Logger.getInstance("app");
const dbLogger = Logger.getInstance("db");

2. Thread-Local Singleton

class ThreadLocalSingleton {
  private static instances = new WeakMap();

  static getInstance(thread: Thread): ThreadLocalSingleton {
    if (!this.instances.has(thread)) {
      this.instances.set(thread, new ThreadLocalSingleton());
    }
    return this.instances.get(thread);
  }
}

Related Patterns

  • Factory: Creates instances; Singleton restricts to one
  • Builder: Constructs complex objects; Singleton controls instantiation
  • Facade: Provides simple interface; Singleton ensures single instance
  • Module Pattern: Similar encapsulation without explicit class

Summary

Singleton Pattern:

  • ✅ Ensures one instance of a class
  • ✅ Provides global access to that instance
  • ❌ Creates global state (testing nightmare)
  • Violates SRP (mixing creation with logic)

Key Takeaway: "Use Singleton sparingly. Prefer Dependency Injection in modern applications."

When to use: Logging, configuration, database connections
When not to: Business logic, services, repositories

Next Steps: Practice implementing thread-safe singletons and testing strategies.


Interview Wisdom: "Singleton is on every interview questions list, but the best answer is often 'use dependency injection instead!'"