SOLUTION: Challenge 3 - Static Singleton Pattern ================================================== Challenge: Create a Database class with a static getInstance() method that returns a single instance (singleton). Add static initialization logic. --- SOLUTION: class Database { private static instance: Database | null = null; private static isInitializing: boolean = false; private connectionString: string = ""; private isConnected: boolean = false; private pool: string[] = []; // Private constructor prevents new Database() private constructor() { console.log("Database instance created (private constructor)"); } // Static factory method static getInstance(): Database { if (Database.instance === null) { Database.instance = new Database(); Database.instance.initialize(); } return Database.instance; } // Static initialization static initialize(connectionString: string = "localhost:5432"): void { if (Database.isInitializing) { console.warn("Database is already initializing..."); return; } Database.isInitializing = true; console.log(`🔧 Initializing database with connection: ${connectionString}`); const instance = Database.getInstance(); instance.connectionString = connectionString; instance.connect(); Database.isInitializing = false; console.log("✅ Database initialization complete"); } // Static utility method static reset(): void { console.log("🔄 Resetting database singleton"); Database.instance = null; Database.isInitializing = false; } // Instance methods private connect(): void { if (this.isConnected) { console.warn("Already connected"); return; } console.log(`📡 Connecting to ${this.connectionString}...`); this.isConnected = true; this.pool = ["connection-1", "connection-2", "connection-3"]; console.log("✅ Connected to database"); } public query(sql: string): any[] { if (!this.isConnected) { throw new Error("Database is not connected"); } console.log(`📝 Executing query: ${sql}`); return [{ id: 1, name: "Result" }]; } public getConnectionStatus(): string { return `Connected: ${this.isConnected}, Pool size: ${this.pool.length}`; } public disconnect(): void { if (!this.isConnected) { console.warn("Already disconnected"); return; } console.log("🔌 Disconnecting from database"); this.isConnected = false; this.pool = []; } } // Usage: // Initialize on startup Database.initialize("postgres://user:pass@localhost:5432/mydb"); // Get singleton instance (always the same) const db1 = Database.getInstance(); const db2 = Database.getInstance(); console.log(db1 === db2); // true — same instance // Use the database const results = db1.query("SELECT * FROM users"); console.log(results); console.log(db1.getConnectionStatus()); // Even if you try to create a new instance with new, it fails // const db3 = new Database(); // ❌ Error: constructor is private // Reset (for testing) Database.reset(); const db3 = Database.getInstance(); console.log(db1 === db3); // false — new instance after reset --- EXPLANATION: SINGLETON PATTERN: Goal: Ensure only one instance of a class exists. Three key components: 1. PRIVATE CONSTRUCTOR private constructor() { } Prevents anyone from calling new Database() 2. STATIC INSTANCE private static instance: Database | null = null; Class-level storage for the single instance 3. STATIC FACTORY METHOD static getInstance(): Database { ... } The ONLY way to get an instance Creates one on first call, returns same one on subsequent calls WHY USE SINGLETON? Resources that should be shared: - Database connections (expensive to create) - Logger instances (one log per application) - Configuration managers (one config per app) - Thread pools (don't create many) Example problems without singleton: - Each module creates its own database connection → wasteful - Logger writes to different files → confused logs - Config loaded multiple times → inconsistent state With singleton: - One connection for entire app - One logger for entire app - One config for entire app STATIC INITIALIZATION: Database.initialize(connectionString); Separate from getInstance() for flexibility: 1. You might want to initialize before getting the instance 2. You can pass configuration at startup time 3. You can guard against double-initialization STATIC RESET (for testing): Database.reset(); Clears the singleton so tests can start fresh. Never use this in production, only in test cleanup. --- THREAD SAFETY (TypeScript/JavaScript note): JavaScript is single-threaded, so the simple singleton pattern is safe. In multi-threaded languages (Java, C#), you'd need locks. --- COMPARISON: SINGLETON VS GLOBAL VARIABLE Without singleton (global database object): const db = new Database(); // Accidentally created twice export db; With singleton: Database.getInstance(); // Always the same instance Singleton is better because: 1. Lazy initialization (only created when first needed) 2. Can't accidentally create multiple instances 3. Clear intent (this is a singleton) 4. Can add logic (reset, initialization flags, etc.) --- ADVANCED: Lazy Loading This singleton is eager (initialized when getInstance() is called). You could also do lazy initialization: static getInstance(): Database { if (Database.instance === null) { console.log("Creating database instance..."); Database.instance = new Database(); } return Database.instance; } This defers creation until it's actually needed. --- REAL-WORLD USAGE: This pattern is used for: - Logger instances (winston, pino create singletons) - Database connections (ORMs, connection pools) - Configuration managers (dotenv, config libraries) - Cache managers (Redis clients) - Event buses (global event emitters) Singleton is one of the most common patterns in professional software.