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
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.
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
✅ 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)
class Database {
private static instance = new Database();
private constructor() {}
static getInstance(): Database {
return Database.instance;
}
}class Database {
private static instance?: Database;
private constructor() {}
static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
}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;
}
}// 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✅ 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)
❌ 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
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;
}
}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);
}
}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);Answer: Yes! Several ways:
1. Reflection (in languages that support it):
// Not possible in TypeScript without special tricks2. Cloning:
const singleton = Logger.getInstance();
const clone = Object.create(singleton); // Creates a copy!3. Serialization:
// Deserializing can create a new instance4. Multiple threads (without synchronization):
// As discussed above - race conditionAnswer: 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 mockDependency 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)
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); // trueThis is thread-safe in JavaScript/TypeScript because module loading is synchronized.
- Logging: Single logger instance for entire app
- Database Connections: Connection pool singleton
- Configuration: Single config object
- Thread Pools: Single executor service
- Caches: Single cache instance
- Event Bus: Single global event emitter
- Application Settings: One settings object
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!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!❌ 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();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!
});
});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");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);
}
}- 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
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!'"