Creational Design Patterns
Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. They abstract the instantiation process and help make a system independent of how its objects are created, composed, and represented.
Factory Method Pattern
The Factory Method defines an interface for creating an object but lets subclasses decide which class to instantiate. It provides a way to delegate instantiation logic to child classes.
// Factory Method: Create different notification channels
interface Notification {
send(to: string, message: string): Promise;
}
class EmailNotification implements Notification {
constructor(private readonly smtpClient: SmtpClient) {}
async send(to: string, message: string): Promise {
await this.smtpClient.send({ to, subject: "Notification", body: message });
}
}
class SmsNotification implements Notification {
constructor(private readonly smsClient: SmsClient) {}
async send(to: string, message: string): Promise {
await this.smsClient.send({ phoneNumber: to, text: message });
}
}
class PushNotification implements Notification {
constructor(private readonly pushClient: PushClient) {}
async send(to: string, message: string): Promise {
await this.pushClient.send({ deviceToken: to, body: message });
}
}
// Factory
class NotificationFactory {
static create(channel: "email" | "sms" | "push"): Notification {
switch (channel) {
case "email": return new EmailNotification(new SmtpClient());
case "sms": return new SmsNotification(new SmsClient());
case "push": return new PushNotification(new PushClient());
default: throw new Error(`Unknown channel: ${channel}`);
}
}
}
// Usage
const notification = NotificationFactory.create("email");
await notification.send("user@example.com", "Your order has shipped!");
Abstract Factory Pattern
// Abstract Factory: Create families of related objects
interface UIFactory {
createButton(): Button;
createInput(): Input;
createModal(): Modal;
}
interface Button {
render(): string;
onClick(handler: () => void): void;
}
interface Input {
render(): string;
getValue(): string;
}
// Material Design family
class MaterialUIFactory implements UIFactory {
createButton(): Button { return new MaterialButton(); }
createInput(): Input { return new MaterialInput(); }
createModal(): Modal { return new MaterialModal(); }
}
// Ant Design family
class AntUIFactory implements UIFactory {
createButton(): Button { return new AntButton(); }
createInput(): Input { return new AntInput(); }
createModal(): Modal { return new AntModal(); }
}
// The app works with any UI family
class FormRenderer {
constructor(private readonly factory: UIFactory) {}
renderLoginForm(): string {
const emailInput = this.factory.createInput();
const passwordInput = this.factory.createInput();
const submitButton = this.factory.createButton();
return `${emailInput.render()}${passwordInput.render()}${submitButton.render()}`;
}
}
// Switch entire UI framework by changing the factory
const materialForm = new FormRenderer(new MaterialUIFactory());
const antForm = new FormRenderer(new AntUIFactory());
Builder Pattern
The Builder pattern separates the construction of a complex object from its representation. It is especially useful when an object has many optional parameters or when the construction process involves multiple steps.
// Builder: Construct complex query objects
class QueryBuilder {
private _table: string = "";
private _selects: string[] = [];
private _wheres: Array<{ column: string; operator: string; value: unknown }> = [];
private _orderBy: Array<{ column: string; direction: "ASC" | "DESC" }> = [];
private _limit?: number;
private _offset?: number;
private _joins: string[] = [];
from(table: string): this {
this._table = table;
return this;
}
select(...columns: string[]): this {
this._selects.push(...columns);
return this;
}
where(column: string, operator: string, value: unknown): this {
this._wheres.push({ column, operator, value });
return this;
}
orderBy(column: string, direction: "ASC" | "DESC" = "ASC"): this {
this._orderBy.push({ column, direction });
return this;
}
limit(count: number): this {
this._limit = count;
return this;
}
offset(count: number): this {
this._offset = count;
return this;
}
join(table: string, on: string): this {
this._joins.push(`JOIN ${table} ON ${on}`);
return this;
}
build(): { sql: string; params: unknown[] } {
const params: unknown[] = [];
let sql = `SELECT ${this._selects.length ? this._selects.join(", ") : "*"} FROM ${this._table}`;
if (this._joins.length) {
sql += " " + this._joins.join(" ");
}
if (this._wheres.length) {
const conditions = this._wheres.map((w, i) => {
params.push(w.value);
return `${w.column} ${w.operator} $${i + 1}`;
});
sql += " WHERE " + conditions.join(" AND ");
}
if (this._orderBy.length) {
sql += " ORDER BY " + this._orderBy.map(o => `${o.column} ${o.direction}`).join(", ");
}
if (this._limit) sql += ` LIMIT ${this._limit}`;
if (this._offset) sql += ` OFFSET ${this._offset}`;
return { sql, params };
}
}
// Fluent API usage
const query = new QueryBuilder()
.from("orders")
.select("id", "customer_id", "total", "status")
.where("status", "=", "active")
.where("total", ">", 100)
.orderBy("created_at", "DESC")
.limit(20)
.offset(40)
.build();
// Also great for configuration objects
class ServerConfigBuilder {
private config: Partial = {};
port(port: number): this { this.config.port = port; return this; }
host(host: string): this { this.config.host = host; return this; }
cors(origins: string[]): this { this.config.corsOrigins = origins; return this; }
rateLimit(max: number, windowMs: number): this {
this.config.rateLimit = { max, windowMs };
return this;
}
tls(cert: string, key: string): this {
this.config.tls = { cert, key };
return this;
}
build(): ServerConfig {
if (!this.config.port) throw new Error("Port is required");
return this.config as ServerConfig;
}
}
const config = new ServerConfigBuilder()
.port(3000)
.host("0.0.0.0")
.cors(["https://example.com"])
.rateLimit(100, 60000)
.build();
Singleton Pattern
// Singleton: Ensure only one instance exists
class DatabasePool {
private static instance: DatabasePool | null = null;
private pool: Pool;
private constructor(connectionString: string) {
this.pool = new Pool({ connectionString, max: 20 });
}
static getInstance(): DatabasePool {
if (!DatabasePool.instance) {
DatabasePool.instance = new DatabasePool(process.env.DATABASE_URL!);
}
return DatabasePool.instance;
}
async query(sql: string, params?: unknown[]): Promise {
const result = await this.pool.query(sql, params);
return result.rows;
}
// For testing: allow resetting the singleton
static resetInstance(): void {
DatabasePool.instance = null;
}
}
// Modern alternative: Module-scoped singleton (preferred in TypeScript)
// database.ts
let pool: Pool | null = null;
export function getPool(): Pool {
if (!pool) {
pool = new Pool({ connectionString: process.env.DATABASE_URL!, max: 20 });
}
return pool;
}
// ES modules are evaluated once — this is a natural singleton
Prototype Pattern
// Prototype: Clone objects to avoid expensive creation
interface Cloneable {
clone(): T;
}
class DocumentTemplate implements Cloneable {
constructor(
public title: string,
public content: string,
public styles: Record,
public metadata: Record
) {}
clone(): DocumentTemplate {
return new DocumentTemplate(
this.title,
this.content,
{ ...this.styles }, // Shallow copy
JSON.parse(JSON.stringify(this.metadata)) // Deep copy
);
}
}
// Template registry
class TemplateRegistry {
private templates = new Map();
register(name: string, template: DocumentTemplate): void {
this.templates.set(name, template);
}
create(templateName: string): DocumentTemplate {
const template = this.templates.get(templateName);
if (!template) throw new Error(`Template not found: ${templateName}`);
return template.clone(); // Clone instead of creating from scratch
}
}
const registry = new TemplateRegistry();
registry.register("invoice", new DocumentTemplate(
"Invoice", "Invoice
", { fontSize: "12px" }, { version: 1 }
));
const myInvoice = registry.create("invoice");
myInvoice.title = "Invoice #1234"; // Modify the clone freely
When to Use Each Pattern
- Factory: When the exact type of object to create is determined at runtime
- Abstract Factory: When you need to create families of related objects consistently
- Builder: When constructing complex objects with many optional parameters
- Singleton: When exactly one instance is needed (database pools, loggers, config)
- Prototype: When creating objects by cloning is more efficient than constructing from scratch