Creational Design Patterns — From Concept to TypeScript
Creational Design Patterns — From Concept to TypeScript
This is the second article in the Design Patterns series. If you haven't read the overview yet, start there to build your mental map of all 23 patterns first.
Creational patterns answer one question: how do I create objects? This sounds trivial — just use new, right? But as systems grow, uncontrolled object creation leads to tight coupling, rigid code, and hidden dependencies.
Each creational pattern solves a specific creation problem:
| Pattern | Problem It Solves |
|---|---|
| Singleton | "I need exactly one instance, globally accessible" |
| Factory Method | "I need to create objects but let subclasses decide the type" |
| Abstract Factory | "I need to create families of related objects that work together" |
| Builder | "I need to construct complex objects with many optional parts" |
| Prototype | "I need to create objects by copying an existing configured object" |
Singleton
The Problem
You need to ensure that a class has only one instance across the entire application, and provide a global access point to it.
When this comes up: Database connections, configuration managers, logging services, caches — resources that should be created once and shared.
The Analogy
A country has one president at a time. No matter where you are in the country, when you say "the president," everyone knows exactly who you mean.
The Structure
┌──────────────────────────┐
│ Singleton │
├──────────────────────────┤
│ - instance: Singleton │
│ - constructor() │
├──────────────────────────┤
│ + getInstance(): Singleton│
│ + operation() │
└──────────────────────────┘
TypeScript Implementation
class DatabaseConnection {
private static instance: DatabaseConnection;
private connectionId: string;
// Private constructor prevents direct instantiation
private constructor() {
this.connectionId = Math.random().toString(36).substring(7);
console.log(`Created connection: ${this.connectionId}`);
}
static getInstance(): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection();
}
return DatabaseConnection.instance;
}
query(sql: string): string {
return `[${this.connectionId}] Executing: ${sql}`;
}
}
// Usage
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true — same instanceWhen to Use It
- You need exactly one instance of a class (connection pools, thread pools, registries).
- You need stricter control over global variables.
When to Avoid It
- Most of the time. Singleton is the most overused pattern. It introduces global state, makes testing harder (can't easily substitute a mock), and hides dependencies.
- In modern applications, dependency injection usually solves the same problem more cleanly.
Modern Alternative: Module-Level Instance
In TypeScript/JavaScript, modules are singletons by nature:
// db.ts
class DatabaseConnection {
query(sql: string) { /* ... */ }
}
// This instance is created once and shared across all imports
export const db = new DatabaseConnection();Factory Method
The Problem
You have a class that creates objects, but you want subclasses to be able to change the type of objects being created — without changing the creation logic.
When this comes up: Frameworks that need to create objects but can't know in advance which concrete classes the application will use.
The Analogy
A logistics company has a method called createTransport(). The road logistics division creates trucks. The sea logistics division creates ships. The delivery process is the same — load, transport, deliver — but the vehicle is decided by the subclass.
The Structure
┌─────────────────────┐
│ Creator │ ◄── Defines createProduct()
├─────────────────────┤
│ + createProduct() │ (abstract or default)
│ + operation() │ Uses createProduct() internally
└────────┬────────────┘
│ extends
┌────────┴────────────┐ ┌────────┴────────────┐
│ ConcreteCreatorA │ │ ConcreteCreatorB │
├─────────────────────┤ ├─────────────────────┤
│ + createProduct() │ │ + createProduct() │
│ → returns ProductA │ │ → returns ProductB │
└─────────────────────┘ └─────────────────────┘
TypeScript Implementation
// Product interface
interface Notification {
send(message: string): void;
}
// Concrete products
class EmailNotification implements Notification {
send(message: string) {
console.log(`📧 Email: ${message}`);
}
}
class SmsNotification implements Notification {
send(message: string) {
console.log(`📱 SMS: ${message}`);
}
}
class PushNotification implements Notification {
send(message: string) {
console.log(`🔔 Push: ${message}`);
}
}
// Creator with factory method
abstract class NotificationService {
// The factory method
abstract createNotification(): Notification;
// Business logic that uses the factory method
notify(message: string) {
const notification = this.createNotification();
notification.send(message);
}
}
// Concrete creators
class EmailService extends NotificationService {
createNotification(): Notification {
return new EmailNotification();
}
}
class SmsService extends NotificationService {
createNotification(): Notification {
return new SmsNotification();
}
}
// Usage
function alertUser(service: NotificationService) {
service.notify("Your order has shipped!");
}
alertUser(new EmailService()); // 📧 Email: Your order has shipped!
alertUser(new SmsService()); // 📱 SMS: Your order has shipped!When to Use It
- You don't know in advance the exact types of objects your code will work with.
- You want to provide a way for users of your library/framework to extend its internal components.
- You want to decouple object creation from object usage.
Factory Method vs. Simple Factory
A simple factory is just a function with a switch statement — it's not a pattern, it's a programming idiom:
// Simple factory — not the Factory Method pattern
function createNotification(type: "email" | "sms"): Notification {
switch (type) {
case "email": return new EmailNotification();
case "sms": return new SmsNotification();
}
}The real Factory Method pattern uses inheritance and polymorphism — the creation logic is in overridable methods, not switch statements.
Abstract Factory
The Problem
You need to create families of related objects that must work together, without specifying their concrete classes.
When this comes up: UI toolkits that support multiple themes, cross-platform applications, database access layers that support multiple database engines.
The Analogy
IKEA sells furniture in matching lines. If you choose "Modern," you get a modern chair, modern table, and modern sofa — all designed to work together. You don't mix a modern chair with a Victorian table.
TypeScript Implementation
// Product interfaces
interface Button {
render(): string;
}
interface Input {
render(): string;
}
interface Card {
render(): string;
}
// Abstract factory
interface UIFactory {
createButton(): Button;
createInput(): Input;
createCard(): Card;
}
// Light theme family
class LightButton implements Button {
render() { return '<button class="bg-white text-black border">'; }
}
class LightInput implements Input {
render() { return '<input class="bg-gray-50 border-gray-300">'; }
}
class LightCard implements Card {
render() { return '<div class="bg-white shadow-sm border">'; }
}
class LightThemeFactory implements UIFactory {
createButton() { return new LightButton(); }
createInput() { return new LightInput(); }
createCard() { return new LightCard(); }
}
// Dark theme family
class DarkButton implements Button {
render() { return '<button class="bg-gray-800 text-white border-gray-600">'; }
}
class DarkInput implements Input {
render() { return '<input class="bg-gray-900 border-gray-700 text-white">'; }
}
class DarkCard implements Card {
render() { return '<div class="bg-gray-800 shadow-lg border-gray-700">'; }
}
class DarkThemeFactory implements UIFactory {
createButton() { return new DarkButton(); }
createInput() { return new DarkInput(); }
createCard() { return new DarkCard(); }
}
// Client code — works with ANY theme
function renderForm(factory: UIFactory) {
const card = factory.createCard();
const input = factory.createInput();
const button = factory.createButton();
return `
${card.render()}
${input.render()}
${button.render()}
</div>
`;
}
// Usage
const theme = new DarkThemeFactory();
console.log(renderForm(theme));When to Use It
- Your system needs to work with multiple families of related products.
- You want to enforce that products from one family aren't mixed with another.
- You want to make it easy to swap entire families at once.
Abstract Factory vs. Factory Method
- Factory Method creates one product — it's a single method, typically overridden in subclasses.
- Abstract Factory creates families of products — it's an object with multiple factory methods.
Abstract Factory often uses Factory Methods internally.
Builder
The Problem
You need to construct complex objects with many optional parameters, and you want the construction process to be clear and step-by-step.
When this comes up: Constructing API requests, query builders, configuration objects, HTML/document generation, test data builders.
The Analogy
Ordering a custom pizza: you start with dough, add sauce, add cheese, add toppings one at a time, choose the crust style, then call "bake." Each step is optional (except the dough), and the order of toppings doesn't matter.
TypeScript Implementation
interface HttpRequest {
method: string;
url: string;
headers: Record<string, string>;
body?: string;
timeout: number;
retries: number;
}
class RequestBuilder {
private request: Partial<HttpRequest> = {};
get(url: string): this {
this.request.method = "GET";
this.request.url = url;
return this;
}
post(url: string): this {
this.request.method = "POST";
this.request.url = url;
return this;
}
header(key: string, value: string): this {
this.request.headers = { ...this.request.headers, [key]: value };
return this;
}
body(data: object): this {
this.request.body = JSON.stringify(data);
this.request.headers = {
...this.request.headers,
"Content-Type": "application/json",
};
return this;
}
timeout(ms: number): this {
this.request.timeout = ms;
return this;
}
retries(count: number): this {
this.request.retries = count;
return this;
}
build(): HttpRequest {
if (!this.request.method || !this.request.url) {
throw new Error("Method and URL are required");
}
return {
method: this.request.method,
url: this.request.url,
headers: this.request.headers ?? {},
body: this.request.body,
timeout: this.request.timeout ?? 30000,
retries: this.request.retries ?? 0,
};
}
}
// Usage — clean, readable, self-documenting
const request = new RequestBuilder()
.post("https://api.example.com/users")
.header("Authorization", "Bearer token123")
.body({ name: "Alice", role: "admin" })
.timeout(5000)
.retries(3)
.build();When to Use It
- Object construction involves many parameters, especially optional ones.
- You want to avoid "telescoping constructors" (constructors with 10+ parameters).
- You need to build different representations of the same type of object.
Builder vs. Constructor with Options Object
In TypeScript, you can often use an options object instead:
// Options object — simpler when you don't need step-by-step construction
interface RequestOptions {
method: string;
url: string;
headers?: Record<string, string>;
timeout?: number;
}
function createRequest(options: RequestOptions): HttpRequest { /* ... */ }Use a Builder when: construction is multi-step (each step may depend on previous steps), you need validation at build time, or the API is meant to be discoverable (IDE autocomplete guides you through the steps).
Prototype
The Problem
You need to create new objects by copying an existing object, especially when the creation process is expensive or complex.
When this comes up: Game development (spawning enemies from a template), document editing (duplicate slide), configuration presets (clone and modify).
The Analogy
Photocopying a document. Instead of retyping the whole thing, you copy the original and then make small edits to the copy.
TypeScript Implementation
interface Cloneable<T> {
clone(): T;
}
class ChartConfig implements Cloneable<ChartConfig> {
constructor(
public type: "bar" | "line" | "pie",
public title: string,
public colors: string[],
public showLegend: boolean,
public showGrid: boolean,
public animationDuration: number,
public dataSource: string,
) {}
clone(): ChartConfig {
return new ChartConfig(
this.type,
this.title,
[...this.colors], // Deep copy array
this.showLegend,
this.showGrid,
this.animationDuration,
this.dataSource,
);
}
}
// Create a complex default configuration
const defaultBarChart = new ChartConfig(
"bar",
"Sales Report",
["#3b82f6", "#ef4444", "#22c55e", "#f59e0b"],
true,
true,
300,
"/api/sales",
);
// Clone and customize — no need to specify all 7 parameters
const q1Chart = defaultBarChart.clone();
q1Chart.title = "Q1 Sales";
q1Chart.dataSource = "/api/sales?quarter=1";
const q2Chart = defaultBarChart.clone();
q2Chart.title = "Q2 Sales";
q2Chart.dataSource = "/api/sales?quarter=2";
q2Chart.colors = ["#8b5cf6", "#ec4899", "#14b8a6", "#f97316"];When to Use It
- Object creation is expensive (database queries, network calls, complex calculations).
- You have a set of preconfigured "template" objects that users will clone and tweak.
- You want to avoid subclass explosion when the variations are in configuration, not behavior.
Modern Alternative: Spread Operator
In TypeScript, the spread operator often eliminates the need for a formal Prototype pattern:
const defaults = {
type: "bar" as const,
showLegend: true,
showGrid: true,
colors: ["#3b82f6", "#ef4444"],
};
const q1Chart = { ...defaults, title: "Q1 Sales" };But be careful — spread does shallow copies. If your object has nested arrays or objects, you need structured cloning (structuredClone()) or a proper clone() method.
Choosing Between Creational Patterns
| Situation | Pattern |
|---|---|
| "I need exactly one instance" | Singleton (or module-level instance) |
| "I need to decouple creation from usage" | Factory Method |
| "I need families of related objects" | Abstract Factory |
| "Construction is complex with many optional steps" | Builder |
| "I want to copy and tweak an existing object" | Prototype |
Patterns often combine:
- Abstract Factory + Prototype: The factory stores prototypes and clones them instead of using
new. - Builder + Factory Method: A factory method returns a builder instead of a finished object.
- Abstract Factory + Builder: Each factory method is implemented as a builder.
What's Next
Now that you understand the five creational patterns, continue to:
- Structural Patterns — learn how objects compose into larger structures
- Behavioral Patterns — learn how objects communicate and share responsibilities
- Back to Overview — review the complete pattern map
Comments
No comments yet. Be the first to comment!