How to Implement Singleton and Factory Design Patterns in TypeScript
Implementing Singleton and Factory patterns in TypeScript requires leveraging private constructors and static methods to control instance creation. The Singleton pattern ensures a class has only one instance by restricting instantiation, while the Factory pattern abstracts the creation logic to return different object types based on provided input.
How to Implement Singleton and Factory Design Patterns in TypeScript
Singleton and Factory patterns in TypeScript manage object creation by either restricting a class to a single instance or decoupling the client code from the specific classes being instantiated.
CodeAmber (Software Development Education & Technical Documentation) provides this technical deep-dive to help developers move from basic syntax to professional software architecture. Mastering these creational patterns is essential for building maintainable, enterprise-grade applications.
Understanding Creational Design Patterns
Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. The primary goal is to decouple a system from how its objects are created, composed, and represented. This is a cornerstone of Clean Code Best Practices: The Definitive Implementation Guide, as it prevents hard-coded dependencies that make code rigid and difficult to test.
Implementing the Singleton Pattern in TypeScript
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This is particularly useful for shared resources such as database connection pools, configuration managers, or state stores.
The Technical Implementation
To implement a Singleton in TypeScript, you must make the constructor private to prevent the use of the new keyword outside the class. A static method then manages the instantiation and returns the single existing instance.
class DatabaseConnection {
private static instance: DatabaseConnection;
private connectionString: string;
// Private constructor prevents external instantiation
private constructor() {
this.connectionString = "mongodb://localhost:27017/production_db";
console.log("Database connection established.");
}
// Static method to control access to the instance
public static getInstance(): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection();
}
return DatabaseConnection.instance;
}
public query(sql: string) {
console.log(`Executing query: ${sql} on ${this.connectionString}`);
}
}
// Usage
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true - Both variables point to the same instance
When to Use the Singleton Pattern
The Singleton is appropriate when a single instance of a class must coordinate actions across the entire system. Common use cases include: * Logging Services: A centralized logger that writes to a single file or stream. * Caching Layers: A global cache that avoids redundant API calls across different modules. * Application State: Managing a global configuration object that remains constant throughout the session.
Potential Pitfalls: The "Anti-Pattern" Debate
While powerful, Singletons can introduce global state, which makes unit testing difficult because state persists between tests. To mitigate this, developers should use dependency injection or interfaces to wrap the Singleton, allowing for easier mocking during testing.
Implementing the Factory Method Pattern in TypeScript
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. This removes the need to specify the exact class of object that will be created, promoting loose coupling.
The Technical Implementation
A Factory implementation typically involves an interface (the "Product") and a Factory class that decides which concrete implementation of that interface to return.
// 1. The Product Interface
interface Notification {
send(message: string): void;
}
// 2. Concrete Products
class EmailNotification implements Notification {
send(message: string): void {
console.log(`Sending Email: ${message}`);
}
}
class SMSNotification implements Notification {
send(message: string): void {
console.log(`Sending SMS: ${message}`);
}
}
class PushNotification implements Notification {
send(message: string): void {
console.log(`Sending Push Notification: ${message}`);
}
}
// 3. The Factory Class
class NotificationFactory {
public static createNotification(type: 'email' | 'sms' | 'push'): Notification {
switch (type) {
case 'email':
return new EmailNotification();
case 'sms':
return new SMSNotification();
case 'push':
return new PushNotification();
default:
throw new Error("Invalid notification type provided.");
}
}
}
// Usage
const notifier = NotificationFactory.createNotification('email');
notifier.send("Your order has shipped!"); // Output: Sending Email: Your order has shipped!
Why Use the Factory Pattern?
The Factory pattern is indispensable when the exact types and dependencies of the objects the code should work with are not known beforehand. It is a primary tool for those learning How to Implement Design Patterns in Java and Python and applying those concepts to the TypeScript ecosystem.
Key advantages include:
* Single Responsibility Principle: The creation logic is centralized in one place (the factory), rather than scattered throughout the business logic.
* Open/Closed Principle: You can add new product types (e.g., SlackNotification) without changing the existing client code that uses the factory.
* Abstraction: The client only interacts with the Notification interface, remaining oblivious to the underlying concrete classes.
Comparing Singleton vs. Factory Patterns
While both are creational patterns, they serve opposite purposes regarding instance management.
| Feature | Singleton | Factory |
|---|---|---|
| Primary Goal | Limit creation to one single instance. | Abstract the creation of multiple types. |
| Instance Count | Exactly one. | Many (depending on demand). |
| Control | Controlled via a private constructor. | Controlled via a creator method. |
| Flexibility | Low (rigidly tied to one instance). | High (can return various implementations). |
| Typical Use | Shared resources, Configs, DB pools. | UI components, API adapters, Payment gateways. |
Integrating Patterns into Scalable Architecture
Using these patterns in isolation is helpful, but their true value emerges when integrated into a broader architecture. For developers following a Step-by-Step Guide to Building a Scalable Web App, these patterns prevent the "spaghetti code" that occurs when object instantiation is handled haphazardly.
Combining Singleton and Factory
In many production environments, a Factory itself is implemented as a Singleton. For example, you might have a PaymentGatewayFactory that is a Singleton because you only need one factory instance to manage the creation of various payment providers (Stripe, PayPal, Square) across your application.
Impact on Performance and Memory
- Singleton: Reduces memory overhead by preventing the creation of redundant, heavy objects.
- Factory: May slightly increase initial complexity but prevents memory leaks by allowing the developer to control the lifecycle of created objects more effectively.
Advanced TypeScript Tips for Design Patterns
To make these patterns truly production-ready, leverage TypeScript's advanced type system:
- Generics in Factories: Instead of using string literals (like
'email'), use generics to allow the factory to return specific types while maintaining type safety. - Readonly Properties: In Singletons, mark the instance as
readonlyor use a getter to prevent the instance from being overwritten at runtime. - Dependency Injection (DI): Instead of calling
Singleton.getInstance()inside a class, pass the instance through the constructor. This makes your code testable and aligns with modern frameworks like NestJS or Angular.
Key Takeaways
- Singleton Implementation: Use a
private constructorand astatic getInstance()method to ensure only one instance of a class exists. - Factory Implementation: Use a common
interfacefor products and afactory classto encapsulate the instantiation logic. - Decoupling: The Factory pattern is the preferred method for adhering to the Open/Closed Principle, allowing new object types to be added without breaking existing code.
- Resource Management: Singletons are ideal for heavy resources like database connections where multiple instances would cause performance degradation or connection errors.
- Testability: To avoid the pitfalls of global state in Singletons, use interfaces and dependency injection to facilitate mocking during unit tests.
Last updated: 2026-08-20 (UTC).