+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 282 of 354

๐Ÿ“˜ Singleton Pattern: Ensuring Single Instance

Master singleton pattern: ensuring single instance in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript ๐Ÿ“
  • TypeScript installation โšก
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand the concept fundamentals ๐ŸŽฏ
  • Apply the concept in real projects ๐Ÿ—๏ธ
  • Debug common issues ๐Ÿ›
  • Write type-safe code โœจ

๐ŸŽฏ Introduction

Hey there, TypeScript explorer! ๐Ÿ‘‹ Have you ever been to a party where thereโ€™s only ONE DJ booth? ๐ŸŽง No matter how many people want to play music, they all have to use the same equipment. Thatโ€™s exactly what the Singleton pattern is all about โ€“ ensuring thereโ€™s only ONE instance of something in your entire application!

Today, weโ€™re diving into one of the most famous (and sometimes controversial) design patterns. Whether youโ€™re building a game engine ๐ŸŽฎ, managing app settings โš™๏ธ, or handling database connections ๐Ÿ—„๏ธ, the Singleton pattern is your ticket to maintaining control and consistency. Letโ€™s master this powerful pattern together! ๐Ÿ’ช

๐Ÿ“š Understanding Singleton Pattern

Think of the Singleton pattern like the president of a country ๐Ÿ›๏ธ โ€“ there can only be ONE at any given time! No matter how many times people refer to โ€œthe president,โ€ theyโ€™re all talking about the same person. Thatโ€™s the essence of Singleton!

What Makes a Singleton Special? ๐ŸŒŸ

The Singleton pattern ensures:

  • Single Instance: Only ONE instance exists throughout your appโ€™s lifetime
  • Global Access: Available from anywhere in your application
  • Lazy Initialization: Created only when first needed
  • Thread Safety: Handles concurrent access properly (weโ€™ll cover this!)

Itโ€™s like having a single remote control ๐Ÿ“บ for your TV โ€“ everyone in the house uses the same one!

๐Ÿ”ง Basic Syntax and Usage

Letโ€™s start with a simple Singleton implementation in TypeScript! ๐Ÿš€

// ๐Ÿ‘‹ Basic Singleton class
class ConfigManager {
  private static instance: ConfigManager;
  private config: Record<string, any> = {};

  // ๐Ÿšซ Private constructor prevents direct instantiation
  private constructor() {
    console.log("๐ŸŽ‰ ConfigManager initialized!");
  }

  // ๐ŸŽฏ Static method to get the instance
  public static getInstance(): ConfigManager {
    if (!ConfigManager.instance) {
      ConfigManager.instance = new ConfigManager();
    }
    return ConfigManager.instance;
  }

  // ๐Ÿ“ Add configuration
  public set(key: string, value: any): void {
    this.config[key] = value;
  }

  // ๐Ÿ“– Get configuration
  public get(key: string): any {
    return this.config[key];
  }
}

// ๐ŸŽฎ Let's use it!
const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();

console.log(config1 === config2); // true โœ… Same instance!

config1.set("theme", "๐ŸŒ™ dark");
console.log(config2.get("theme")); // "๐ŸŒ™ dark" - Same data!

See how both config1 and config2 point to the exact same instance? Thatโ€™s the magic! โœจ

๐Ÿ’ก Practical Examples

Letโ€™s explore some real-world scenarios where Singleton shines! ๐ŸŒŸ

Example 1: Game Score Manager ๐ŸŽฎ

// ๐Ÿ† Managing game scores across your app
class ScoreManager {
  private static instance: ScoreManager;
  private scores: Map<string, number> = new Map();
  private highScore: number = 0;

  private constructor() {
    console.log("๐ŸŽฏ ScoreManager ready!");
  }

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

  // ๐Ÿ“ˆ Add player score
  public addScore(player: string, points: number): void {
    const currentScore = this.scores.get(player) || 0;
    const newScore = currentScore + points;
    this.scores.set(player, newScore);
    
    // ๐Ÿ… Update high score if needed
    if (newScore > this.highScore) {
      this.highScore = newScore;
      console.log(`๐ŸŽŠ New high score: ${newScore} by ${player}!`);
    }
  }

  // ๐Ÿ“Š Get leaderboard
  public getLeaderboard(): Array<{player: string, score: number}> {
    return Array.from(this.scores.entries())
      .map(([player, score]) => ({ player, score }))
      .sort((a, b) => b.score - a.score);
  }

  // ๐Ÿ† Get high score
  public getHighScore(): number {
    return this.highScore;
  }
}

// ๐ŸŽฎ Game components using the singleton
class GameLevel {
  private scoreManager = ScoreManager.getInstance();

  completeLevel(player: string, levelScore: number): void {
    console.log(`โœ… Level complete! Adding ${levelScore} points`);
    this.scoreManager.addScore(player, levelScore);
  }
}

class BonusRound {
  private scoreManager = ScoreManager.getInstance();

  collectBonus(player: string, bonus: number): void {
    console.log(`๐ŸŽ Bonus collected! +${bonus} points`);
    this.scoreManager.addScore(player, bonus);
  }
}

// ๐ŸŽฏ Playing the game
const level = new GameLevel();
const bonus = new BonusRound();

level.completeLevel("Alice ๐Ÿฆ„", 100);
bonus.collectBonus("Alice ๐Ÿฆ„", 50);
level.completeLevel("Bob ๐Ÿป", 120);

const scores = ScoreManager.getInstance();
console.log("๐Ÿ† Leaderboard:", scores.getLeaderboard());

Example 2: Theme Manager for Your App ๐ŸŽจ

// ๐ŸŽจ Managing app theme consistently
interface Theme {
  name: string;
  primary: string;
  secondary: string;
  background: string;
  text: string;
}

class ThemeManager {
  private static instance: ThemeManager;
  private currentTheme: Theme;
  private themes: Map<string, Theme> = new Map();
  private listeners: Array<(theme: Theme) => void> = [];

  private constructor() {
    // ๐ŸŒˆ Initialize with default themes
    this.themes.set("light", {
      name: "โ˜€๏ธ Light",
      primary: "#3498db",
      secondary: "#2ecc71",
      background: "#ffffff",
      text: "#2c3e50"
    });

    this.themes.set("dark", {
      name: "๐ŸŒ™ Dark",
      primary: "#9b59b6",
      secondary: "#e74c3c",
      background: "#2c3e50",
      text: "#ecf0f1"
    });

    this.themes.set("ocean", {
      name: "๐ŸŒŠ Ocean",
      primary: "#006994",
      secondary: "#00a8cc",
      background: "#e8f5f5",
      text: "#004e71"
    });

    this.currentTheme = this.themes.get("light")!;
  }

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

  // ๐ŸŽจ Get current theme
  public getCurrentTheme(): Theme {
    return this.currentTheme;
  }

  // ๐Ÿ”„ Switch theme
  public switchTheme(themeName: string): void {
    const theme = this.themes.get(themeName);
    if (theme) {
      this.currentTheme = theme;
      console.log(`โœจ Switched to ${theme.name} theme!`);
      this.notifyListeners();
    }
  }

  // ๐Ÿ‘‚ Subscribe to theme changes
  public subscribe(listener: (theme: Theme) => void): void {
    this.listeners.push(listener);
  }

  // ๐Ÿ“ข Notify all listeners
  private notifyListeners(): void {
    this.listeners.forEach(listener => listener(this.currentTheme));
  }

  // ๐Ÿ“ Get available themes
  public getAvailableThemes(): string[] {
    return Array.from(this.themes.keys());
  }
}

// ๐ŸŽจ UI Components using the theme
class Header {
  private themeManager = ThemeManager.getInstance();

  constructor() {
    this.themeManager.subscribe((theme) => {
      console.log(`๐Ÿ“ฑ Header updated with ${theme.name} colors!`);
    });
  }

  render(): void {
    const theme = this.themeManager.getCurrentTheme();
    console.log(`๐ŸŽจ Header: Background ${theme.primary}, Text ${theme.text}`);
  }
}

class Sidebar {
  private themeManager = ThemeManager.getInstance();

  constructor() {
    this.themeManager.subscribe((theme) => {
      console.log(`๐Ÿ“‚ Sidebar updated with ${theme.name} colors!`);
    });
  }
}

// ๐ŸŽฎ Using the theme system
const header = new Header();
const sidebar = new Sidebar();
const theme = ThemeManager.getInstance();

header.render();
theme.switchTheme("dark");
theme.switchTheme("ocean");

Example 3: Database Connection Pool ๐Ÿ—„๏ธ

// ๐Ÿ—„๏ธ Managing database connections efficiently
interface DBConnection {
  id: string;
  isActive: boolean;
  query: (sql: string) => Promise<any>;
}

class DatabasePool {
  private static instance: DatabasePool;
  private connections: DBConnection[] = [];
  private maxConnections: number = 5;
  private activeQueries: number = 0;

  private constructor() {
    console.log("๐Ÿš€ Database pool initialized!");
    this.initializeConnections();
  }

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

  // ๐Ÿ”ง Initialize connection pool
  private initializeConnections(): void {
    for (let i = 0; i < this.maxConnections; i++) {
      this.connections.push({
        id: `conn-${i + 1}`,
        isActive: false,
        query: async (sql: string) => {
          // ๐ŸŽญ Simulate database query
          console.log(`๐Ÿ“Š Executing: ${sql}`);
          await new Promise(resolve => setTimeout(resolve, 100));
          return { success: true, data: "Query result ๐Ÿ“ฆ" };
        }
      });
    }
  }

  // ๐ŸŽฏ Get available connection
  private async getConnection(): Promise<DBConnection> {
    // ๐Ÿ” Find available connection
    let connection = this.connections.find(conn => !conn.isActive);
    
    if (!connection) {
      console.log("โณ All connections busy, waiting...");
      await new Promise(resolve => setTimeout(resolve, 200));
      return this.getConnection();
    }

    connection.isActive = true;
    this.activeQueries++;
    console.log(`๐Ÿ”— Using connection: ${connection.id}`);
    return connection;
  }

  // ๐Ÿ“Š Execute query
  public async executeQuery(sql: string): Promise<any> {
    const connection = await this.getConnection();
    
    try {
      const result = await connection.query(sql);
      console.log(`โœ… Query completed on ${connection.id}`);
      return result;
    } finally {
      // ๐Ÿ”“ Release connection
      connection.isActive = false;
      this.activeQueries--;
      console.log(`๐Ÿ”“ Released connection: ${connection.id}`);
    }
  }

  // ๐Ÿ“ˆ Get pool statistics
  public getStats(): { total: number; active: number; available: number } {
    const active = this.connections.filter(conn => conn.isActive).length;
    return {
      total: this.maxConnections,
      active,
      available: this.maxConnections - active
    };
  }
}

// ๐ŸŽฎ Using the database pool
async function runDatabaseOperations() {
  const db = DatabasePool.getInstance();

  // ๐Ÿ“Š Multiple queries from different parts of the app
  const queries = [
    db.executeQuery("SELECT * FROM users ๐Ÿ‘ฅ"),
    db.executeQuery("SELECT * FROM products ๐Ÿ“ฆ"),
    db.executeQuery("SELECT * FROM orders ๐Ÿ›’"),
    db.executeQuery("UPDATE inventory SET stock = stock - 1 ๐Ÿ“‰"),
    db.executeQuery("INSERT INTO logs VALUES ('Action logged') ๐Ÿ“"),
    db.executeQuery("SELECT * FROM analytics ๐Ÿ“Š")
  ];

  console.log("๐Ÿš€ Starting queries...");
  console.log("๐Ÿ“ˆ Pool stats:", db.getStats());

  await Promise.all(queries);

  console.log("๐ŸŽ‰ All queries completed!");
  console.log("๐Ÿ“ˆ Final pool stats:", db.getStats());
}

// ๐Ÿƒโ€โ™‚๏ธ Run the example
runDatabaseOperations();

๐Ÿš€ Advanced Concepts

Ready to level up your Singleton game? Letโ€™s explore some advanced techniques! ๐ŸŽฏ

Thread-Safe Singleton (for async operations) ๐Ÿ”’

// ๐Ÿ”’ Thread-safe singleton with async initialization
class AsyncSingleton {
  private static instance: AsyncSingleton;
  private static initPromise: Promise<AsyncSingleton>;
  private data: any;

  private constructor() {}

  // ๐Ÿš€ Async initialization
  private async initialize(): Promise<void> {
    console.log("โณ Starting async initialization...");
    // ๐ŸŽญ Simulate async operation (like loading config from API)
    await new Promise(resolve => setTimeout(resolve, 1000));
    this.data = { 
      apiKey: "๐Ÿ”‘ secret-key-123",
      serverUrl: "๐ŸŒ https://api.example.com"
    };
    console.log("โœ… Initialization complete!");
  }

  // ๐ŸŽฏ Get instance with async initialization
  public static async getInstance(): Promise<AsyncSingleton> {
    if (!AsyncSingleton.instance) {
      if (!AsyncSingleton.initPromise) {
        // ๐Ÿ”’ Ensure only one initialization happens
        AsyncSingleton.initPromise = (async () => {
          AsyncSingleton.instance = new AsyncSingleton();
          await AsyncSingleton.instance.initialize();
          return AsyncSingleton.instance;
        })();
      }
      return AsyncSingleton.initPromise;
    }
    return AsyncSingleton.instance;
  }

  public getData(): any {
    return this.data;
  }
}

// ๐ŸŽฎ Using async singleton
async function useAsyncSingleton() {
  console.log("๐Ÿš€ Getting instance 1...");
  const instance1 = await AsyncSingleton.getInstance();
  
  console.log("๐Ÿš€ Getting instance 2...");
  const instance2 = await AsyncSingleton.getInstance();
  
  console.log("Same instance?", instance1 === instance2); // true โœ…
  console.log("Data:", instance1.getData());
}

useAsyncSingleton();

Singleton with Dependency Injection ๐Ÿ’‰

// ๐Ÿ’‰ Singleton that can be configured
interface Logger {
  log(message: string): void;
}

class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(`๐Ÿ“ Console: ${message}`);
  }
}

class FileLogger implements Logger {
  log(message: string): void {
    console.log(`๐Ÿ“„ File: ${message}`);
  }
}

class LogManager {
  private static instance: LogManager;
  private logger: Logger;
  private logHistory: string[] = [];

  private constructor(logger: Logger) {
    this.logger = logger;
  }

  // ๐ŸŽฏ Configure and get instance
  public static configure(logger: Logger): void {
    if (!LogManager.instance) {
      LogManager.instance = new LogManager(logger);
    } else {
      console.warn("โš ๏ธ LogManager already configured!");
    }
  }

  public static getInstance(): LogManager {
    if (!LogManager.instance) {
      // ๐ŸŽฏ Default to console logger
      LogManager.instance = new LogManager(new ConsoleLogger());
    }
    return LogManager.instance;
  }

  // ๐Ÿ“ Log with history
  public log(level: string, message: string): void {
    const timestamp = new Date().toISOString();
    const logEntry = `[${timestamp}] ${level}: ${message}`;
    
    this.logger.log(logEntry);
    this.logHistory.push(logEntry);
    
    // ๐Ÿงน Keep only last 100 entries
    if (this.logHistory.length > 100) {
      this.logHistory.shift();
    }
  }

  // ๐Ÿ“Š Get log history
  public getHistory(): string[] {
    return [...this.logHistory];
  }

  // ๐ŸŽจ Log with style
  public info(message: string): void {
    this.log("โ„น๏ธ INFO", message);
  }

  public warn(message: string): void {
    this.log("โš ๏ธ WARN", message);
  }

  public error(message: string): void {
    this.log("โŒ ERROR", message);
  }

  public success(message: string): void {
    this.log("โœ… SUCCESS", message);
  }
}

// ๐ŸŽฎ Configure and use
LogManager.configure(new FileLogger());

const log = LogManager.getInstance();
log.info("Application started");
log.success("User logged in");
log.warn("Low memory");
log.error("Connection failed");

console.log("๐Ÿ“œ Log history:", log.getHistory());

Registry Pattern (Multiple Named Singletons) ๐Ÿ“š

// ๐Ÿ“š Managing multiple singleton instances
class ServiceRegistry {
  private static instance: ServiceRegistry;
  private services: Map<string, any> = new Map();

  private constructor() {}

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

  // ๐Ÿ“ Register a service
  public register<T>(name: string, factory: () => T): void {
    if (this.services.has(name)) {
      console.warn(`โš ๏ธ Service '${name}' already registered!`);
      return;
    }
    
    // ๐Ÿ”’ Lazy singleton creation
    let instance: T | null = null;
    const serviceGetter = () => {
      if (!instance) {
        instance = factory();
        console.log(`โœจ Created singleton service: ${name}`);
      }
      return instance;
    };
    
    this.services.set(name, serviceGetter);
  }

  // ๐ŸŽฏ Get a service
  public get<T>(name: string): T {
    const serviceGetter = this.services.get(name);
    if (!serviceGetter) {
      throw new Error(`โŒ Service '${name}' not found!`);
    }
    return serviceGetter();
  }

  // ๐Ÿ“Š List registered services
  public listServices(): string[] {
    return Array.from(this.services.keys());
  }
}

// ๐ŸŽจ Example services
class EmailService {
  send(to: string, message: string): void {
    console.log(`๐Ÿ“ง Sending email to ${to}: ${message}`);
  }
}

class NotificationService {
  notify(user: string, message: string): void {
    console.log(`๐Ÿ”” Notifying ${user}: ${message}`);
  }
}

class CacheService {
  private cache: Map<string, any> = new Map();
  
  set(key: string, value: any): void {
    this.cache.set(key, value);
    console.log(`๐Ÿ’พ Cached: ${key}`);
  }
  
  get(key: string): any {
    return this.cache.get(key);
  }
}

// ๐ŸŽฎ Register and use services
const registry = ServiceRegistry.getInstance();

registry.register("email", () => new EmailService());
registry.register("notifications", () => new NotificationService());
registry.register("cache", () => new CacheService());

// ๐Ÿš€ Use services from anywhere
const email1 = registry.get<EmailService>("email");
const email2 = registry.get<EmailService>("email");
console.log("Same email service?", email1 === email2); // true โœ…

email1.send("[email protected]", "Welcome! ๐ŸŽ‰");

const notifications = registry.get<NotificationService>("notifications");
notifications.notify("Alice", "New message! ๐Ÿ’ฌ");

const cache = registry.get<CacheService>("cache");
cache.set("user:123", { name: "Bob ๐Ÿป", level: 42 });

console.log("๐Ÿ“‹ Available services:", registry.listServices());

โš ๏ธ Common Pitfalls and Solutions

Letโ€™s avoid the common mistakes developers make with Singletons! ๐Ÿ›ก๏ธ

โŒ Wrong: Multiple Instance Creation

// โŒ BAD: Forgetting to make constructor private
class BadSingleton {
  static instance: BadSingleton;

  constructor() { // ๐Ÿšจ Public constructor!
    console.log("Creating instance...");
  }

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

// ๐Ÿ˜ฑ This creates multiple instances!
const bad1 = new BadSingleton(); // Oops!
const bad2 = new BadSingleton(); // Double oops!
const bad3 = BadSingleton.getInstance();

console.log(bad1 === bad2); // false โŒ

โœ… Correct: Private Constructor

// โœ… GOOD: Private constructor prevents direct instantiation
class GoodSingleton {
  private static instance: GoodSingleton;

  private constructor() { // ๐Ÿ”’ Private!
    console.log("โœจ Singleton created!");
  }

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

// const wrong = new GoodSingleton(); // ๐Ÿšซ TypeScript error!
const good1 = GoodSingleton.getInstance();
const good2 = GoodSingleton.getInstance();

console.log(good1 === good2); // true โœ…

โŒ Wrong: Testing Difficulties

// โŒ BAD: Hard to test singleton
class HardToTestService {
  private static instance: HardToTestService;
  
  private constructor() {}
  
  static getInstance(): HardToTestService {
    if (!this.instance) {
      this.instance = new HardToTestService();
    }
    return this.instance;
  }

  fetchData(): string {
    // ๐Ÿšจ Hard-coded dependency!
    return "production data";
  }
}

โœ… Correct: Testable Singleton

// โœ… GOOD: Testable singleton with reset capability
class TestableSingleton {
  private static instance: TestableSingleton;
  private dataSource: () => string;

  private constructor(dataSource?: () => string) {
    this.dataSource = dataSource || (() => "production data");
  }

  public static getInstance(dataSource?: () => string): TestableSingleton {
    if (!TestableSingleton.instance) {
      TestableSingleton.instance = new TestableSingleton(dataSource);
    }
    return TestableSingleton.instance;
  }

  // ๐Ÿงช For testing only
  public static resetInstance(): void {
    TestableSingleton.instance = null as any;
  }

  public fetchData(): string {
    return this.dataSource();
  }
}

// ๐Ÿงช In tests
TestableSingleton.resetInstance();
const testInstance = TestableSingleton.getInstance(() => "test data");
console.log(testInstance.fetchData()); // "test data" โœ…

๐Ÿ› ๏ธ Best Practices

Here are the golden rules for using Singletons effectively! ๐Ÿ†

1. Use Singletons Sparingly ๐ŸŽฏ

// ๐Ÿ‘ Good use cases:
// - Application configuration
// - Logger instances
// - Connection pools
// - Cache managers

// ๐Ÿ‘Ž Avoid for:
// - Business logic classes
// - Data models
// - UI components

2. Make It Thread-Safe ๐Ÿ”’

class ThreadSafeSingleton {
  private static instance: ThreadSafeSingleton;
  private static isInitializing = false;

  private constructor() {}

  public static async getInstance(): Promise<ThreadSafeSingleton> {
    if (!ThreadSafeSingleton.instance) {
      if (!ThreadSafeSingleton.isInitializing) {
        ThreadSafeSingleton.isInitializing = true;
        ThreadSafeSingleton.instance = new ThreadSafeSingleton();
        ThreadSafeSingleton.isInitializing = false;
      } else {
        // โณ Wait for initialization
        while (ThreadSafeSingleton.isInitializing) {
          await new Promise(resolve => setTimeout(resolve, 10));
        }
      }
    }
    return ThreadSafeSingleton.instance;
  }
}

3. Consider Dependency Injection ๐Ÿ’‰

// ๐ŸŽฏ Better than hard-coded singletons
interface IConfigService {
  get(key: string): any;
}

class AppComponent {
  constructor(private config: IConfigService) {
    // ๐Ÿ’ช Easy to test and swap implementations
  }
}

4. Document Singleton Usage ๐Ÿ“

/**
 * ๐Ÿ”’ Application-wide settings manager (Singleton)
 * 
 * @example
 * const settings = SettingsManager.getInstance();
 * settings.set('theme', 'dark');
 * 
 * @singleton This class maintains a single instance
 */
class SettingsManager {
  // Implementation...
}

5. Provide Cleanup Methods ๐Ÿงน

class ResourceManager {
  private static instance: ResourceManager;
  private resources: any[] = [];

  private constructor() {}

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

  // ๐Ÿงน Clean up resources
  public cleanup(): void {
    console.log("๐Ÿงน Cleaning up resources...");
    this.resources.forEach(resource => resource.dispose?.());
    this.resources = [];
  }

  // ๐Ÿ”„ Reset for testing
  public static reset(): void {
    if (ResourceManager.instance) {
      ResourceManager.instance.cleanup();
      ResourceManager.instance = null as any;
    }
  }
}

๐Ÿงช Hands-On Exercise

Time to put your singleton skills to the test! ๐Ÿ’ช

๐ŸŽฏ Challenge: Build a Multi-Feature App Manager

Create a singleton AppManager that:

  1. Manages user sessions
  2. Tracks app analytics
  3. Handles feature flags
  4. Provides a plugin system

Requirements:

  • Single instance throughout the app
  • Thread-safe initialization
  • Plugin registration system
  • Analytics event tracking
  • Feature flag management

Starter Code:

interface Plugin {
  name: string;
  init(): void;
}

interface AnalyticsEvent {
  name: string;
  data?: any;
  timestamp: Date;
}

// ๐ŸŽฏ Your challenge: Complete this implementation!
class AppManager {
  // TODO: Implement singleton pattern
  // TODO: Add user session management
  // TODO: Add analytics tracking
  // TODO: Add feature flags
  // TODO: Add plugin system
}

// Test your implementation:
// const app = AppManager.getInstance();
// app.login("user123");
// app.track("page_view", { page: "home" });
// app.isFeatureEnabled("dark_mode");
// app.registerPlugin(myPlugin);
๐Ÿ“ Click to see the solution
interface Plugin {
  name: string;
  init(): void;
}

interface AnalyticsEvent {
  name: string;
  data?: any;
  timestamp: Date;
}

interface UserSession {
  userId: string;
  loginTime: Date;
  isActive: boolean;
}

class AppManager {
  private static instance: AppManager;
  private static initPromise: Promise<AppManager>;
  
  private currentSession: UserSession | null = null;
  private analytics: AnalyticsEvent[] = [];
  private featureFlags: Map<string, boolean> = new Map();
  private plugins: Map<string, Plugin> = new Map();
  private initialized = false;

  private constructor() {}

  // ๐ŸŽฏ Thread-safe async initialization
  private async initialize(): Promise<void> {
    if (this.initialized) return;
    
    console.log("๐Ÿš€ Initializing AppManager...");
    
    // ๐ŸŽญ Simulate async config loading
    await new Promise(resolve => setTimeout(resolve, 100));
    
    // ๐Ÿณ๏ธ Load default feature flags
    this.featureFlags.set("dark_mode", true);
    this.featureFlags.set("beta_features", false);
    this.featureFlags.set("analytics", true);
    
    this.initialized = true;
    console.log("โœ… AppManager ready!");
  }

  // ๐Ÿ”’ Get singleton instance
  public static async getInstance(): Promise<AppManager> {
    if (!AppManager.instance) {
      if (!AppManager.initPromise) {
        AppManager.initPromise = (async () => {
          AppManager.instance = new AppManager();
          await AppManager.instance.initialize();
          return AppManager.instance;
        })();
      }
      return AppManager.initPromise;
    }
    return AppManager.instance;
  }

  // ๐Ÿ‘ค User session management
  public login(userId: string): void {
    if (this.currentSession?.isActive) {
      this.logout();
    }
    
    this.currentSession = {
      userId,
      loginTime: new Date(),
      isActive: true
    };
    
    console.log(`๐Ÿ‘ค User ${userId} logged in!`);
    this.track("user_login", { userId });
  }

  public logout(): void {
    if (this.currentSession) {
      const sessionDuration = Date.now() - this.currentSession.loginTime.getTime();
      this.track("user_logout", { 
        userId: this.currentSession.userId,
        sessionDuration 
      });
      
      console.log(`๐Ÿ‘‹ User ${this.currentSession.userId} logged out!`);
      this.currentSession = null;
    }
  }

  public getCurrentUser(): string | null {
    return this.currentSession?.userId || null;
  }

  // ๐Ÿ“Š Analytics tracking
  public track(eventName: string, data?: any): void {
    if (!this.isFeatureEnabled("analytics")) {
      return;
    }

    const event: AnalyticsEvent = {
      name: eventName,
      data,
      timestamp: new Date()
    };
    
    this.analytics.push(event);
    console.log(`๐Ÿ“Š Tracked: ${eventName}`, data || "");
    
    // ๐Ÿงน Keep only last 1000 events
    if (this.analytics.length > 1000) {
      this.analytics = this.analytics.slice(-1000);
    }
  }

  public getAnalytics(): AnalyticsEvent[] {
    return [...this.analytics];
  }

  // ๐Ÿณ๏ธ Feature flags
  public isFeatureEnabled(feature: string): boolean {
    return this.featureFlags.get(feature) || false;
  }

  public setFeatureFlag(feature: string, enabled: boolean): void {
    this.featureFlags.set(feature, enabled);
    console.log(`๐Ÿณ๏ธ Feature '${feature}' ${enabled ? "enabled" : "disabled"}`);
    this.track("feature_flag_changed", { feature, enabled });
  }

  // ๐Ÿ”Œ Plugin system
  public registerPlugin(plugin: Plugin): void {
    if (this.plugins.has(plugin.name)) {
      console.warn(`โš ๏ธ Plugin '${plugin.name}' already registered!`);
      return;
    }
    
    this.plugins.set(plugin.name, plugin);
    plugin.init();
    console.log(`๐Ÿ”Œ Plugin '${plugin.name}' registered!`);
    this.track("plugin_registered", { pluginName: plugin.name });
  }

  public getPlugin(name: string): Plugin | undefined {
    return this.plugins.get(name);
  }

  // ๐Ÿ“ˆ App statistics
  public getStats(): any {
    return {
      currentUser: this.getCurrentUser(),
      totalEvents: this.analytics.length,
      activeFeatures: Array.from(this.featureFlags.entries())
        .filter(([_, enabled]) => enabled)
        .map(([feature]) => feature),
      loadedPlugins: Array.from(this.plugins.keys()),
      sessionActive: this.currentSession?.isActive || false
    };
  }
}

// ๐Ÿงช Test the implementation
async function testAppManager() {
  // ๐ŸŽฏ Get instance
  const app1 = await AppManager.getInstance();
  const app2 = await AppManager.getInstance();
  
  console.log("Same instance?", app1 === app2); // true โœ…
  
  // ๐Ÿ‘ค User management
  app1.login("alice_123");
  console.log("Current user:", app1.getCurrentUser());
  
  // ๐Ÿ“Š Analytics
  app1.track("page_view", { page: "dashboard" });
  app1.track("button_click", { button: "save" });
  
  // ๐Ÿณ๏ธ Feature flags
  console.log("Dark mode enabled?", app1.isFeatureEnabled("dark_mode"));
  app1.setFeatureFlag("beta_features", true);
  
  // ๐Ÿ”Œ Plugins
  const chatPlugin: Plugin = {
    name: "chat",
    init() {
      console.log("๐Ÿ’ฌ Chat plugin initialized!");
    }
  };
  
  const mapPlugin: Plugin = {
    name: "maps",
    init() {
      console.log("๐Ÿ—บ๏ธ Maps plugin initialized!");
    }
  };
  
  app1.registerPlugin(chatPlugin);
  app2.registerPlugin(mapPlugin); // Same instance!
  
  // ๐Ÿ“ˆ Stats
  console.log("๐Ÿ“Š App Stats:", app1.getStats());
  
  // ๐Ÿ‘‹ Logout
  app1.logout();
}

// ๐Ÿš€ Run the test
testAppManager();

๐ŸŽ Bonus Challenge

Extend the AppManager with:

  1. Notification system with subscription
  2. Cache manager with TTL (time-to-live)
  3. Error tracking with stack traces
  4. Performance monitoring

๐ŸŽ“ Key Takeaways

Youโ€™ve mastered the Singleton pattern! Hereโ€™s what youโ€™ve learned: ๐ŸŽ‰

โœ… Singleton ensures only ONE instance exists throughout your app
โœ… Private constructors prevent direct instantiation
โœ… Static getInstance() provides global access point
โœ… Thread-safe implementations handle concurrent access
โœ… Real-world uses include config, logging, and connection pools

Remember: With great power comes great responsibility! Use singletons wisely โ€“ theyโ€™re powerful but can make testing harder if overused. ๐Ÿ•ท๏ธ

๐Ÿค Next Steps

Congratulations on mastering the Singleton pattern! ๐ŸŽŠ Hereโ€™s how to continue your design patterns journey:

  1. ๐Ÿญ Explore Factory Pattern - Learn how to create objects without specifying their exact class
  2. ๐Ÿ”จ Try Builder Pattern - Master complex object construction step by step
  3. ๐ŸŽญ Study Observer Pattern - Implement event-driven architectures
  4. ๐Ÿงฉ Practice Adapter Pattern - Make incompatible interfaces work together

Keep coding, keep learning, and remember โ€“ youโ€™re building amazing things! ๐Ÿš€โœจ

Happy Singleton-ing! ๐Ÿ’ช๐ŸŽฏ