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:
- Double-check locking: Prevents multiple instances
- Volatile: Ensures visibility across threads
- Lazy initialization: Created only when needed
- 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.