Skip to main content
  1. Examples/

The Singleton Pattern - Guaranteeing a Single Instance

·11 mins· loading · loading · · ·
Design Back-End
Adrien D'acunto
Author
Adrien D’acunto
Table of Contents

The Singleton Pattern: Guaranteeing a Single Instance
#

Introduction
#

The Singleton is one of the most well-known (and sometimes controversial) design patterns from the Gang of Four. Its objective is simple: ensure that a class has only one instance throughout the entire application and provide a global point of access to that instance.

In this article, we will explore the Singleton pattern through a concrete example: a Coordinator class that can only exist in a single exemplar in the system.

What is the Singleton?
#

Definition
#

The Singleton pattern ensures that a class:

  • Has only one instance during the entire application lifetime
  • Provides a global point of access to that instance
  • Controls its own instantiation through a private constructor

UML Structure
#

classDiagram
    class Singleton {
        -static instance: Singleton
        -constructor()
        +static getInstance() Singleton
        +businessMethod()
    }
    
    note for Singleton "Private constructor\nStatic instance\nStatic getter"
Characteristic Description
Private constructor Prevents direct instantiation with new
Static instance Stores the unique instance of the class
Static method getInstance() returns the unique instance
Lazy initialization Instance is created on first demand

Use Case: The Unique Coordinator
#

Context
#

Imagine a team management system where:

  • A single coordinator supervises the entire team
  • This coordinator must be accessible from anywhere in the application
  • There must never be two coordinators simultaneously

Problem without Singleton
#

// PROBLEM: Multiple instances possible
const coord1 = new Coordinateur();
const coord2 = new Coordinateur();

// coord1 and coord2 are two different instances
console.log(coord1 === coord2); // false

Consequences:

  • Data inconsistency
  • Confusion about “who is the real coordinator”
  • Resource waste
  • Hard to trace bugs

TypeScript Implementation
#

Complete Architecture
#

classDiagram
    class Person {
        <<abstract>>
        #name: string
        #firstname: string
        #email: string
        #phone: string
        +birthdate: Date
        +gender: string
        -strategy: NameStrategy
        
        +getName() string
        +setName(name: string) void
        +getFirstname() string
        +setFirstname(firstname: string) void
        +getEmail() string
        +setEmail(email: string) void
        +getPhone() string
        +setPhone(phone: string) void
        +getDisplayName() string
        +setStrategy(strategy: NameStrategy) void
        +sayHello()* void
    }
    
    class Coordinateur {
        -static instance: Coordinateur
        -static instanciations: number
        -constructor()
        +static getInstance() Coordinateur
        +sayHello() void
    }
    
    class NameStrategy {
        <<interface>>
        +transform(person: Person) string
    }
    
    class NameFirstStrategy {
        +transform(person: Person) string
    }
    
    Person <|-- Coordinateur : inherits
    Person --> NameStrategy : uses
    NameStrategy <|.. NameFirstStrategy : implements
    
    note for Coordinateur "Singleton Pattern\nOnly one instance possible"

Abstract Person Class
#

import { NameFirstStrategy } from "./strategies/name-first-strategy";
import type { NameStrategy } from "./strategies/name-strategy";

export abstract class Person {
    // Protected attributes (accessible to subclasses)
    protected name: string = '';
    protected firstname: string = '';
    protected email: string = '';
    protected phone: string = '';
    
    // Public attributes
    public birthdate: Date = new Date();
    public gender: string = '';

    // Strategy pattern for name formatting
    private strategy: NameStrategy = new NameFirstStrategy()

    constructor() {}
    
    // Getters and Setters with validation
    public getName(): string {
        return this.name;
    }

    public setName(name: string): void {
        // Prevents modification if already set
        if (this.name === '') {
            this.name = name;
        }
    }

    public getFirstname(): string {
        return this.firstname;
    }

    public setFirstname(firstname: string): void {
        if (this.firstname === '') {
            this.firstname = firstname;
        }
    }

    public getEmail(): string {
        return this.email;
    }

    public setEmail(email: string): void {
        if (this.email === '') {
            this.email = email;
        }
    }

    public getPhone(): string {
        return this.phone;
    }

    public setPhone(phone: string): void {
        if (this.phone === '') {
            this.phone = phone;
        }
    }

    // Using the Strategy pattern
    public getDisplayName(): string {
        return this.strategy.transform(this)
    }

    public setStrategy(strategy: NameStrategy): void {
        this.strategy = strategy
    }

    // Abstract method (must be implemented by subclasses)
    public abstract sayHello(): void;
}

Key points:

  • Abstract class: cannot be instantiated directly
  • Encapsulation: protected attributes with getters/setters
  • Validation: prevents data modification once set
  • Strategy Pattern: configurable name formatting

Singleton Coordinator Class
#

import { Person } from "./person"

export class Coordinateur extends Person {

    // Private static instance (the heart of the Singleton)
    private static instance: Coordinateur | undefined = undefined
    
    // Optional counter to trace instantiation attempts
    private static instanciations: number = 0

    // Private constructor: forbids `new Coordinateur()`
    private constructor() {
        super()
        Coordinateur.instanciations++
    }

    // Static method: only way to get the instance
    public static getInstance(): Coordinateur {
        // Lazy initialization: creation on first demand
        if (Coordinateur.instance === undefined) {
            Coordinateur.instance = new Coordinateur()
        }

        return Coordinateur.instance
    }

    // Implementation of abstract method
    public sayHello(): void {
        console.log(`Hello, I am ${this.getDisplayName()}, the unique coordinator!`);
    }
    
    // Utility method for debugging
    public static getInstanciationCount(): number {
        return Coordinateur.instanciations
    }
}

Name Formatting Strategy (Strategy Pattern)
#

// Strategy Interface
export interface NameStrategy {
    transform(person: Person): string;
}

// Implementation: First name then Last name
export class NameFirstStrategy implements NameStrategy {
    transform(person: Person): string {
        return `${person.getFirstname()} ${person.getName()}`;
    }
}

// Alternative implementation: Last name then First name
export class LastNameFirstStrategy implements NameStrategy {
    transform(person: Person): string {
        return `${person.getName()} ${person.getFirstname()}`;
    }
}

Code Analysis
#

Sequence Diagram: Getting the Instance
#

sequenceDiagram
    participant Client1
    participant Client2
    participant Coordinateur
    
    Note over Client1: First request
    Client1->>Coordinateur: getInstance()
    
    alt Instance does not exist
        Coordinateur->>Coordinateur: new Coordinateur()
        Note over Coordinateur: instance created
    end
    
    Coordinateur-->>Client1: returns instance
    
    Note over Client2: Second request
    Client2->>Coordinateur: getInstance()
    
    alt Instance already exists
        Note over Coordinateur: Returns existing instance
    end
    
    Coordinateur-->>Client2: returns SAME instance
    
    Note over Client1,Client2: client1 === client2

The Private Constructor: The Guardian
#

// Private constructor
private constructor() {
    super()
    Coordinateur.instanciations++
}

Why private?

  • Prevents: new Coordinateur() (compilation error)
  • Forces use of getInstance()
  • Total control over instantiation

Attempted direct instantiation:

// ERROR: Constructor of class 'Coordinateur' is private
const coord = new Coordinateur();

getInstance(): The Single Point of Access
#

public static getInstance(): Coordinateur {
    // Check for existence
    if (Coordinateur.instance === undefined) {
        // Lazy creation (on first demand)
        Coordinateur.instance = new Coordinateur()
    }

    // Always returns the same instance
    return Coordinateur.instance
}

Characteristics:

  • Thread-safe in JavaScript (single-threaded)
  • Lazy initialization: deferred creation
  • Single and controlled point of access

The Instantiation Counter: Debugging
#

private static instanciations: number = 0

public static getInstanciationCount(): number {
    return Coordinateur.instanciations
}

Utility:

  • Verify that the constructor is called only once
  • Trace instantiation attempts
  • Unit tests: validate Singleton behavior

Practical Usage
#

Complete Example
#

// main.ts
import { Coordinateur } from './coordinateur';
import { LastNameFirstStrategy } from './strategies/last-name-first-strategy';

// Get the unique instance
const coord1 = Coordinateur.getInstance();

// Configure the coordinator
coord1.setFirstname('John');
coord1.setName('Doe');
coord1.setEmail('john.doe@example.com');
coord1.birthdate = new Date('1980-05-15');
coord1.gender = 'M';

// Say hello
coord1.sayHello();
// Output: "Hello, I am John Doe, the unique coordinator!"

// Get the instance again (same object)
const coord2 = Coordinateur.getInstance();

console.log(coord1 === coord2); // true
console.log(coord2.getEmail()); // "john.doe@example.com"

// Change the formatting strategy
coord2.setStrategy(new LastNameFirstStrategy());
console.log(coord2.getDisplayName()); // "Doe John"

// Check the number of instantiations
console.log(Coordinateur.getInstanciationCount()); // 1

Unit Tests
#

// coordinateur.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { Coordinateur } from './coordinateur';

describe('Coordinateur Singleton', () => {
  
  it('should return the same instance', () => {
    const coord1 = Coordinateur.getInstance();
    const coord2 = Coordinateur.getInstance();
    
    expect(coord1).toBe(coord2);
  });
  
  it('should instantiate only once', () => {
    const coord = Coordinateur.getInstance();
    
    expect(Coordinateur.getInstanciationCount()).toBe(1);
  });
  
  it('should share state between references', () => {
    const coord1 = Coordinateur.getInstance();
    coord1.setFirstname('Alice');
    
    const coord2 = Coordinateur.getInstance();
    
    expect(coord2.getFirstname()).toBe('Alice');
  });
  
  it('should not allow new Coordinateur()', () => {
    // TypeScript compile-time check
    // @ts-expect-error - Constructor is private
    expect(() => new Coordinateur()).toThrow();
  });
});

Advantages and Disadvantages
#

Advantages
#

Advantage Description Example
Guaranteed unique instance Impossible to create multiple instances Single coordinator
Global access Accessible from anywhere Coordinateur.getInstance()
Strict control Controlled initialization (Lazy loading)
Resource economy Only one instance in memory Memory optimization
Shared state All parts share the same data Global configuration

Disadvantages
#

Disadvantage Description Impact
Global state Disguised global variable Strong coupling
Difficult tests Persistent state between tests Need for reset
SRP violation Manages instantiation + business logic Multiple responsibilities
Concurrency Issues in multi-threading Not applicable in JS
Hidden dependencies Difficult to trace dependencies Complex maintenance

When to Use the Singleton?
#

Use the Singleton for:

// Global configuration
class AppConfig {
    private static instance: AppConfig;
    private config: Record<string, any> = {};
    
    private constructor() {
        // Load config from file
    }
    
    public static getInstance(): AppConfig {
        if (!AppConfig.instance) {
            AppConfig.instance = new AppConfig();
        }
        return AppConfig.instance;
    }
}

// Global logger
class Logger {
    private static instance: Logger;
    
    private constructor() {}
    
    public static getInstance(): Logger {
        if (!Logger.instance) {
            Logger.instance = new Logger();
        }
        return Logger.instance;
    }
    
    public log(message: string): void {
        console.log(`[${new Date().toISOString()}] ${message}`);
    }
}

// Single database connection
class DatabaseConnection {
    private static instance: DatabaseConnection;
    private connection: any;
    
    private constructor() {
        // Establish connection
    }
    
    public static getInstance(): DatabaseConnection {
        if (!DatabaseConnection.instance) {
            DatabaseConnection.instance = new DatabaseConnection();
        }
        return DatabaseConnection.instance;
    }
}

Do NOT use the Singleton for:

// Simple business objects
class User {
    // No need for Singleton
}

// Services with frequently modified state
class ShoppingCart {
    // Each user has their own cart
}

// Objects easily mockable for tests
class ApiClient {
    // Better to inject it as dependency
}

Variants and Alternatives
#

Eager Singleton (Immediate Loading)
#

export class EagerCoordinateur extends Person {
    // Instance created when class is loaded
    private static instance: EagerCoordinateur = new EagerCoordinateur();
    
    private constructor() {
        super();
    }
    
    public static getInstance(): EagerCoordinateur {
        return EagerCoordinateur.instance;
    }
    
    public sayHello(): void {
        console.log("Eager Coordinator loaded!");
    }
}

Differences:

  • Simpler (no verification)
  • Immediate loading (even if never used)
  • Thread-safe by default (if applicable)

Singleton with Reset Method (Tests)
#

export class TestableCoordinateur extends Person {
    private static instance: TestableCoordinateur | undefined;
    
    private constructor() {
        super();
    }
    
    public static getInstance(): TestableCoordinateur {
        if (!TestableCoordinateur.instance) {
            TestableCoordinateur.instance = new TestableCoordinateur();
        }
        return TestableCoordinateur.instance;
    }
    
    // Use ONLY in tests
    public static resetInstance(): void {
        TestableCoordinateur.instance = undefined;
    }
    
    public sayHello(): void {
        console.log("Testable Coordinator!");
    }
}

// In tests
afterEach(() => {
    TestableCoordinateur.resetInstance();
});

Module Singleton (ES6 Pattern)
#

In JavaScript/TypeScript, modules are already singletons:

// coordinateur-module.ts
class CoordinateurClass extends Person {
    constructor() {
        super();
    }
    
    public sayHello(): void {
        console.log("Coordinator via module!");
    }
}

// Export of unique instance
export const coordinateur = new CoordinateurClass();
// Usage
import { coordinateur } from './coordinateur-module';

coordinateur.setFirstname('Marie');
coordinateur.sayHello();

Advantages:

  • Simpler (no getInstance())
  • Idiomatic in JavaScript
  • Easy to mock (named export)

Dependency Injection (Modern Alternative)
#

// IoC Container (e.g.: InversifyJS, TSyringe)
import { injectable, singleton } from 'tsyringe';

@singleton()
@injectable()
export class CoordinateurService extends Person {
    constructor() {
        super();
    }
    
    public sayHello(): void {
        console.log("Coordinator via DI!");
    }
}

// Usage
import { container } from 'tsyringe';

const coord1 = container.resolve(CoordinateurService);
const coord2 = container.resolve(CoordinateurService);

console.log(coord1 === coord2); // true

Advantages:

  • Testable (injection of mocks)
  • Complete decoupling
  • Lifecycle management

Best Practices
#

Avoid Excessive Global State
#

// BAD: Too much business logic in Singleton
class BadCoordinateur {
    private static instance: BadCoordinateur;
    
    public tasks: Task[] = [];
    public team: Member[] = [];
    public projects: Project[] = [];
    // ... 50 properties
    
    // Thousands of lines of business logic
}

// GOOD: Lightweight Singleton that delegates
class GoodCoordinateur {
    private static instance: GoodCoordinateur;
    
    constructor(
        private taskManager: TaskManager,
        private teamManager: TeamManager,
        private projectManager: ProjectManager
    ) {}
}

Make Singleton Testable
#

// Interface for coordinator
export interface ICoordinateur {
    sayHello(): void;
    getDisplayName(): string;
}

// Singleton implementation
export class Coordinateur extends Person implements ICoordinateur {
    private static instance: Coordinateur | undefined;
    
    private constructor() {
        super();
    }
    
    public static getInstance(): ICoordinateur {
        if (!Coordinateur.instance) {
            Coordinateur.instance = new Coordinateur();
        }
        return Coordinateur.instance;
    }
    
    public sayHello(): void {
        console.log("Hello from real Coordinateur");
    }
}

// Mock for tests
export class MockCoordinateur implements ICoordinateur {
    public sayHello(): void {
        console.log("Hello from mock");
    }
    
    public getDisplayName(): string {
        return "Mock Coordinateur";
    }
}

// In tests
it('should work with mock', () => {
    const mock = new MockCoordinateur();
    const service = new SomeService(mock); // Injection
    
    expect(service.doSomething()).toBe('expected');
});

Clear Documentation
#

/**
 * Coordinator - Singleton Pattern
 * 
 * Represents the unique coordinator of the team.
 * Only one instance can exist throughout the entire application.
 * 
 * @example
 * ```typescript
 * const coord = Coordinateur.getInstance();
 * coord.setFirstname('Alice');
 * coord.sayHello();
 * ```
 * 
 * @remarks
 * Never use `new Coordinateur()` (private constructor)
 * Instance is shared globally
 * 
 * @see Person - Base class
 * @see NameStrategy - Name formatting strategy
 */
export class Coordinateur extends Person {
    // ...
}

Logging and Monitoring
#

export class Coordinateur extends Person {
    private static instance: Coordinateur | undefined;
    private static instanciations: number = 0;
    private static accessCount: number = 0;
    
    private constructor() {
        super();
        Coordinateur.instanciations++;
        
        console.log(`[Coordinateur] Instance created (count: ${Coordinateur.instanciations})`);
    }
    
    public static getInstance(): Coordinateur {
        Coordinateur.accessCount++;
        
        if (!Coordinateur.instance) {
            console.log('[Coordinateur] Lazy initialization...');
            Coordinateur.instance = new Coordinateur();
        }
        
        if (Coordinateur.accessCount % 100 === 0) {
            console.log(`[Coordinateur] Accessed ${Coordinateur.accessCount} times`);
        }
        
        return Coordinateur.instance;
    }
    
    public static getStats(): { instanciations: number; accesses: number } {
        return {
            instanciations: Coordinateur.instanciations,
            accesses: Coordinateur.accessCount
        };
    }
    
    public sayHello(): void {
        console.log(`Hello, I am ${this.getDisplayName()}`);
    }
}

Conclusion
#

Summary
#

The Singleton is a powerful pattern but should be used with caution:

Advantages Disadvantages
Guaranteed unique instance Global state
Simplified global access Difficult tests
Strict control Strong coupling
Resource economy SRP violation

When to Use It?
#

Use the Singleton for:

  • Global application configuration
  • Centralized logger
  • Single connection pool
  • Shared cache
  • Unique resource manager (like our Coordinator)

Avoid the Singleton for:

  • Standard business objects
  • Easily testable services
  • Frequently modified state
  • Objects with multiple contexts

The Singleton is like seasoning: used sparingly, it enhances the dish. Used excessively, it ruins it.

The Singleton pattern has its place in your toolbox, but should not be your first reflex. In our Coordinator example, it is justified because:

  • It represents a unique resource by nature
  • Its global access is necessary
  • It simplifies architecture without compromising it

But remain vigilant: a poorly used Singleton can turn your code into a maintenance nightmare.

Related

Single Responsibility Principle (SRP) - Invoice Class Refactoring
·13 mins· loading · loading
Design Back-End
Interfaces in Object-Oriented Programming - SOLID Principle and Practical Example
·6 mins· loading · loading
Design Back-End
Database Modeling - The Langlois Case
·6 mins· loading · loading
Design Back-End
Algorithms - Complete Guide
··23 mins· loading · loading
Back-End Front-End
How Databases Work - Complete Guide
··23 mins· loading · loading
Back-End
Automatic Backlog Documentation Generation with Google Sheets and Apps Script
·2 mins· loading · loading
Documentation