Astrology for Sleep Optimization · CodeAmber

Implementing Singleton and Factory Design Patterns in TypeScript

To implement the Singleton and Factory patterns in TypeScript, use a private constructor and a static instance method for the Singleton to ensure a single class instance exists. For the Factory pattern, create a creator class or function that abstracts the instantiation logic of multiple related classes through a common interface.

Implementing Singleton and Factory Design Patterns in TypeScript

Creational design patterns solve the problem of object creation by decoupling the system from how its objects are instantiated. In TypeScript, the Singleton and Factory patterns are the most frequently utilized tools for managing resource allocation and object complexity.

The Singleton Pattern: Ensuring a Single Instance

The Singleton pattern restricts the instantiation of a class to one single instance. This is critical for shared resources such as database connection pools, configuration managers, or global state stores where multiple instances would lead to memory waste or data inconsistency.

Technical Implementation

To implement a Singleton in TypeScript, you must make the constructor private to prevent the use of the new keyword from outside the class. A static method then manages the lifecycle of the instance.

class DatabaseConnection {
    private static instance: DatabaseConnection;

    // Private constructor prevents external instantiation
    private constructor() {
        console.log("Connecting to database...");
    }

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

    public query(sql: string) {
        console.log(`Executing: ${sql}`);
    }
}

// Usage
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true

When to Use Singleton

Use the Singleton pattern when a single point of truth is required across the entire application. However, developers should use this pattern sparingly, as global state can make unit testing difficult by introducing hidden dependencies between tests.

The Factory Pattern: Abstracting Object Creation

The Factory pattern provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. It is primarily used to handle complex instantiation logic or when the exact type of the object is determined at runtime.

Technical Implementation

The Factory pattern relies on a common interface that all produced objects implement. The Factory class then contains a method—often called create—that returns an object conforming to that interface.

interface Logger {
    log(message: string): void;
}

class FileLogger implements Logger {
    log(message: string) {
        console.log(`Writing to file: ${message}`);
    }
}

class CloudLogger implements Logger {
    log(message: string) {
        console.log(`Sending to cloud: ${message}`);
    }
}

class LoggerFactory {
    public static createLogger(type: 'file' | 'cloud'): Logger {
        if (type === 'file') {
            return new FileLogger();
        } else if (type === 'cloud') {
            return new CloudLogger();
        }
        throw new Error("Logger type not supported.");
    }
}

// Usage
const logger = LoggerFactory.createLogger('cloud');
logger.log("System initialized");

When to Use Factory

The Factory pattern is ideal when your application needs to support multiple variations of a product (e.g., different payment gateways or notification services) without coupling the client code to the specific concrete classes.

Singleton vs. Factory: Decision Matrix

Choosing between these two patterns depends on whether you are managing the number of instances or the type of instances.

Feature Singleton Factory
Primary Goal Control instance count (exactly one). Control object creation logic.
Instantiation Internal (via static method). External (via creator class).
Flexibility Rigid; returns the same instance. Flexible; returns different subtypes.
Typical Use Case Config files, Cache, DB connections. UI Components, API Adapters, Document Parsers.

For a deeper dive into how these interact in real-world projects, see the CodeAmber guide on Implementing Singleton vs. Factory Patterns in TypeScript.

Production Considerations and Best Practices

While these patterns are powerful, improper implementation can lead to "code smells." To maintain a professional codebase, follow these guidelines:

Avoid "Singleton Abuse"

Singletons can act as global variables in disguise. To avoid this, consider using Dependency Injection (DI). Instead of calling DatabaseConnection.getInstance() inside every service, pass the instance into the service's constructor. This makes the code more modular and easier to mock during testing.

Interface-Driven Development

Always return an interface from a Factory rather than a concrete class. This ensures that the calling code remains agnostic of the underlying implementation, adhering to the Dependency Inversion Principle. This approach is a core component of Best Practices for Clean Code in Modern Software Development.

Memory Management

In the Singleton pattern, the instance persists for the lifetime of the application. In high-scale environments, ensure that your Singleton does not hold onto large amounts of unnecessary data, which could lead to memory leaks.

Key Takeaways

Original resource: Visit the source site