Skip to contentJMRG
All posts

Singleton Pattern: A Controlled Global Instance

design-patternscreationalsingleton

What is the Singleton Pattern?

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. It's especially useful when you need to control access to shared resources like database connections, connection pools, or application configurations.

classDiagram
    class Singleton {
        -instance: Singleton$
        -Singleton()
        +getInstance(): Singleton$
        +operation()
    }

    note for Singleton "instance is static\ngetInstance() is static\nConstructor is private"

When to Use Singleton

Use Singleton:

  • Database connections
  • Application configuration
  • Connection pools
  • Centralized loggers

Avoid Singleton:

  • In multi-threaded contexts without proper synchronization
  • When it makes unit testing difficult
  • If it introduces unnecessary global coupling

Step by Step Implementation

1. Private Constructor

The private constructor prevents direct external instantiation with new:

class DatabaseConnection {
  private connectionString: string;

  private constructor(connectionString: string) {
    this.connectionString = connectionString;
  }
}

2. Static Property for the Instance

The class maintains a static reference to its only instance:

private static instance: DatabaseConnection | null = null;

3. Access Method (getInstance)

Global access point that creates the instance only if it doesn't exist (Lazy Initialization):

public static getInstance(connectionString?: string): DatabaseConnection {
  if (!DatabaseConnection.instance) {
    DatabaseConnection.instance = new DatabaseConnection(
      connectionString ?? 'mongodb://localhost:27017/default'
    );
  }
  return DatabaseConnection.instance;
}

4. Reset Method for Testing

Allows resetting the instance in unit tests:

public static resetInstance(): void {
  DatabaseConnection.instance = null;
}

Complete Practical Example

interface User {
  id: string;
  name: string;
}

class DatabaseConnection {
  private static instance: DatabaseConnection | null = null;
  private connectionString: string;
  private connected: boolean = false;

  private constructor(connectionString: string) {
    this.connectionString = connectionString;
  }

  public static getInstance(connectionString?: string): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection(
        connectionString ?? 'mongodb://localhost:27017/default'
      );
    }
    return DatabaseConnection.instance;
  }

  public static resetInstance(): void {
    DatabaseConnection.instance = null;
  }

  async connect(): Promise<void> {
    if (this.connected) return;
    console.log(`Connecting to ${this.connectionString}...`);
    this.connected = true;
  }

  async query<T>(sql: string): Promise<T[]> {
    console.log(`Executing: ${sql}`);
    return [] as T[];
  }
}

// Usage
const db1 = DatabaseConnection.getInstance('mongodb://prod:27017/app');
const db2 = DatabaseConnection.getInstance();

console.log(db1 === db2); // true - same instance

await db1.connect();
const users = await db2.query<User>('SELECT * FROM users');

Variant: Singleton with Symbol

To guarantee uniqueness even across different modules:

const SINGLETON_KEY = Symbol.for('app.singleton.config');

class ConfigurationManager {
  private config: Map<string, unknown> = new Map();

  static get instance(): ConfigurationManager {
    const globalAny = globalThis as Record<symbol, ConfigurationManager>;

    if (!globalAny[SINGLETON_KEY]) {
      globalAny[SINGLETON_KEY] = new ConfigurationManager();
    }

    return globalAny[SINGLETON_KEY];
  }

  set<T>(key: string, value: T): void {
    this.config.set(key, value);
  }

  get<T>(key: string): T | undefined {
    return this.config.get(key) as T;
  }
}

Summary

Aspect Description
Purpose Single instance with global access
Components Private constructor, static instance, getInstance method
Benefits Resource control, consistent shared state
Considerations Can hinder testing, use resetInstance for tests

Conclusion

The Singleton pattern is fundamental for managing shared resources in a controlled way. Use it wisely: it's powerful but can introduce global coupling if overused.

In the next article we'll explore the Factory Method Pattern, which delegates object creation to specialized subclasses.


Based on "Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four).