Singleton Design Pattern

The Singleton pattern is classified as a creational design pattern because its primary purpose is to control the creation of objects. Specifically, the Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance.

The Singleton pattern restricts the instantiation of a class to a single object. This is achieved by making the constructor private, thereby preventing external code from creating new instances of the class.

Singleton provides a globally accessible instance, typically through a static method. This method checks if the instance already exists; if not, it creates and returns it. Otherwise, it returns the existing instance.

Singleton manages the lifecycle of its instance. It decides when the instance is created (usually on first access) and ensures that it persists for the duration of the program (or as needed).

By ensuring that only one instance of the class exists, the Singleton pattern avoids issues related to the creation of multiple instances, such as inconsistent state, excessive resource usage, or conflicts in managing shared resources.

In the context of front-end web development, this pattern can be very useful for managing shared resources like application state, configurations, or service instances.

Practical Example: A Singleton Configuration Manager

Let's create a ConfigManager class that handles more complexity, such as managing different sets of URLs, Google Maps API keys, and potentially other configuration variables for three different environments: development, staging, and production.

Let’s say we are building a web application where various parts of the application need access to a shared configuration (e.g., API keys and URLs). We want to ensure that there is only one instance of this configuration throughout the application to avoid inconsistencies.

We can incorporate the Registry pattern to manage different environment configurations. This way, we can extend or modify the configurations without breaking the Open/Closed Principle (OCP). The Registry pattern complements Singleton by acting as a central storage for various objects or configurations, allowing them to be retrieved when needed. When combined, Singleton and Registry allow for the creation and management of unique instances of objects or configurations, each associated with a specific key or identifier.

Step 1: Create the interface

Step 2: Create the Configuration Classes

Each environment will have its own configuration class. These classes will hold the configuration details specific to that environment.

//Config.ts
export interface Config {
  getApiUrl(): string;
  getGoogleMapsApiKey(): string;
  // Add other configuration methods as needed
}

//DevelopmentConfig.ts
import { Config } from "./Config";

export class DevelopmentConfig implements Config {
  getApiUrl(): string {
    return "https://dev.example.com/api";
  }

  getGoogleMapsApiKey(): string {
    return "DEV_GOOGLE_MAPS_API_KEY";
  }
}


//ProductionConfig.ts
export class ProductionConfig implements Config {
  getApiUrl(): string {
    return "https://prod.example.com/api";
  }

  getGoogleMapsApiKey(): string {
    return "PROD_GOOGLE_MAPS_API_KEY";
  }
}

//StagingConfig.ts
export class StagingConfig implements Config {
  getApiUrl(): string {
    return "https://staging.example.com/api";
  }

  getGoogleMapsApiKey(): string {
    return "STAGING_GOOGLE_MAPS_API_KEY";
  }
}

Step 3: Implement the Registry

The Registry pattern will act as a central place where different environment configurations are registered. It will provide a way to retrieve the appropriate configuration based on the environment.

export class ConfigRegistry {
  private static instance: ConfigRegistry;

  private static configs: { [key: string]: any } = {};

  private constructor() {}

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

  registerConfig(env: string, config: any): void {
    if (!ConfigRegistry.configs[env]) {
      ConfigRegistry.configs[env] = config;
    } else {
      throw new Error(`Configuration for '${env}' is already registered.`);
    }
  }

  getConfig(env: string): any {
    const config = ConfigRegistry.configs[env];

    if (!config) {
      throw new Error(`No configuration registered for environment : ${env}`);
    }

    return config;
  }
}

Step 4: Create the Singleton ConfigManager

The ConfigManager will use the ConfigRegistry to fetch the correct configuration based on the environment. It will be a singleton to ensure that there is only one instance managing the configuration.

import { ConfigRegistry } from "./ConfigRegistry";

export class ConfigManager {
  private static instance: ConfigManager;
  private config: any;

  private constructor(environment: string) {
    const registry = ConfigRegistry.getInstance();
    this.config = registry.getConfig(environment);
  }

  static getInstance(environment: string): ConfigManager {
    if (!ConfigManager.instance) {
      ConfigManager.instance = new ConfigManager(environment);
    }
    return ConfigManager.instance;
  }

  getApiUrl(): string {
    return this.config.getApiUrl();
  }

  getGoogleMapsApiKey(): string {
    return this.config.getGoogleMapsApiKey();
  }
}

Step 5: Register the Configurations

Step 6: Using the ConfigManager

Instantiate the ConfigManager with a specific environment key.

import { ConfigManager } from "./ConfigManager";
import { ConfigRegistry } from "./ConfigRegistry";
import { DevelopmentConfig } from "./DevelopmentConfig";
import { ProductionConfig } from "./ProductionConfig";
import { StagingConfig } from "./StagingConfig";

function main() {
  const registry = ConfigRegistry.getInstance();
  registry.registerConfig("development", new DevelopmentConfig());
  registry.registerConfig("production", new ProductionConfig());
  registry.registerConfig("staging", new StagingConfig());

  //Instantiate the ConfigManager with a specific environment key

  const environment = "production"; // Change this to 'development' or 'staging' as needed
  const configManager = ConfigManager.getInstance(environment);
  console.log(configManager.getApiUrl()); // Logs the API URL for the specified environment
  console.log(configManager.getGoogleMapsApiKey()); // Logs the Google Maps API key for the specified environment
}

main();

Explanation:

  • Interface (Config): Defines the methods that must be implemented by all configuration classes, ensuring a consistent API.

  • Concrete Config Classes (DevelopmentConfig, ProductionConfig, StagingConfig): Implement the Config interface, providing specific configurations for each environment.

  • Registry (ConfigRegistry): Manages the registration and retrieval of configuration instances, ensuring that only one instance of each configuration is used.

  • Singleton Manager (ConfigManager): Retrieves the configuration based on the environment key and provides methods to access configuration values.

Conclusion:

Benefits of Singleton with Registry:

  1. Different configurations or objects. This makes it easy to extend or change configurations without modifying the core logic.

  2. Single Responsibility Principle (SRP): Each class has a single responsibility—ConfigRegistry manages registration and retrieval, while ConfigManager accesses configurations.

  3. Open/Closed Principle (OCP): The design is open for extension. You can add new configurations without modifying existing classes, adhering to OCP.

  4. Singleton Consistency: The Singleton pattern ensures that only one instance of both ConfigRegistry and ConfigManager exists, maintaining consistency across the application.

The GITHUB LINK for the sample code reference.

1
Subscribe to my newsletter

Read articles from Ganesh Rama Hegde directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ganesh Rama Hegde
Ganesh Rama Hegde

Passionate Developer | Code Whisperer | Innovator Hi there! I'm a senior software developer with a love for all things tech and a knack for turning complex problems into elegant, scalable solutions. Whether I'm diving deep into TypeScript, crafting seamless user experiences in React Native, or exploring the latest in cloud computing, I thrive on the thrill of bringing ideas to life through code. I’m all about creating clean, maintainable, and efficient code, with a strong focus on best practices like the SOLID principles. My work isn’t just about writing code; it’s about crafting digital experiences that resonate with users and drive impact. Beyond the code editor, I’m an advocate for continuous learning, always exploring new tools and technologies to stay ahead in this ever-evolving field. When I'm not coding, you'll find me blogging about my latest discoveries, experimenting with side projects, or contributing to open-source communities. Let's connect, share knowledge, and build something amazing together!