Creational Patterns¶
Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. They abstract the instantiation process, making systems independent of how their objects are created, composed, and represented.
Analogy
Creational patterns solve the same problem every manufacturing industry faces: how do you control what gets built, who builds it, and how many exist? A city builds exactly one water treatment plant that everyone connects to (Singleton). A logistics company promises "we'll deliver it" and the local depot decides truck vs. ship (Factory Method). Choosing Apple means your phone, tablet, and watch all work together — pick Samsung and you get a different matching family (Abstract Factory). At Subway, the sandwich artist walks you through bread → protein → veggies → sauce step by step (Builder). A spreadsheet template gets duplicated and tweaked instead of rebuilt from scratch each quarter (Prototype).
Structural Patterns
Object composition and relationships — assembling larger structures while keeping them flexible and efficient.
Behavioral Patterns
Object communication and responsibility distribution — making complex control flows easier to understand.
Singleton¶
Ensures a class has only one instance and provides a global access point to it. Use when exactly one object is needed to coordinate actions across the system — e.g., a configuration manager, connection pool, or logger.
Analogy
Like a city's water treatment plant — you don't build a new one every time someone turns on a tap. Everyone connects to the same facility, and the infrastructure guarantees exactly one exists. This is why database connection pools and loggers use Singleton: creating multiples would waste resources and cause conflicts.
When to Use¶
- You need exactly one instance shared across the system — e.g., database connection pools, Laravel's
config()helper - Multiple instances would cause resource conflicts or waste — e.g., thread pools, print spoolers ensuring serialized hardware access
- You need a global access point to a shared resource — e.g., application registries, Express.js app instance, logging systems
Trade-offs¶
- ✅ Controlled access to a single shared resource — avoids duplicate connections or conflicting state
- ✅ Lazy initialization — created only when first needed, saving startup cost
- ⚠️ Global state makes unit testing harder — mock/stub requires extra setup. Acceptable when the singleton is stateless or read-only (e.g., config, logger), problematic when it holds mutable state across requests.
- ⚠️ Violates Single Responsibility — the class manages its own lifecycle + business logic. In practice, frameworks like Laravel and Spring handle the lifecycle via DI containers, so your class stays clean.
Fitness Test
"Do I need to guarantee exactly one instance across the entire system?"
- Yes → ✅ Singleton fits
- No, I just want convenient access → ❌ Use dependency injection instead
flowchart LR
A[Client] -->|getInstance| B[Singleton]
C[Client] -->|getInstance| B
B -.->|same instance| B
class Database {
private static instance: Database;
private constructor() {
// private to prevent direct instantiation
}
static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
query(sql: string): void {
console.log(`Executing: ${sql}`);
}
}
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true
Factory Method¶
Defines an interface for creating objects but lets subclasses decide which class to instantiate. Use when you don't know ahead of time what type of object you need, or when you want to delegate creation logic to subclasses.
Analogy
Like a logistics company that promises "we'll deliver your package" without telling you whether it goes by truck, drone, or bike. The local depot (subclass) decides based on the route. This is why frameworks use factory methods — Laravel's Storage::disk() doesn't make you specify the driver directly; the configured subclass decides whether it's S3, local, or FTP.
When to Use¶
- You don't know the concrete type ahead of time — e.g., Laravel's
Storage::disk()choosing S3, local, or FTP based on config - You want subclasses to control which objects get created — e.g., notification dispatchers returning email, SMS, or push notifiers
- You need to decouple client code from specific implementations — e.g., database drivers providing MySQL, PostgreSQL, or SQLite connections
Trade-offs¶
- ✅ Open/Closed Principle — add new product types without modifying existing code
- ✅ Decouples creation from usage — client code works with the interface, not concrete classes
- ⚠️ Adds a class hierarchy for each product type. Acceptable when the number of types grows over time (plugin systems, multi-driver frameworks), overkill for a fixed set of 2–3 types.
- ⚠️ Can obscure what's actually being created. In production, log the concrete type at creation time to maintain observability.
Fitness Test
"Will the type of object I create vary based on context, configuration, or subclass?"
- Yes → ✅ Factory Method fits
- No, it's always the same type → ❌ A simple constructor is enough
flowchart TB
A["Logistics (Creator)"] -->|createTransport| B["Transport (Product)"]
C[RoadLogistics] -.->|extends| A
D[SeaLogistics] -.->|extends| A
C -->|creates| E[Truck]
D -->|creates| F[Ship]
E -.->|implements| B
F -.->|implements| B
interface Transport {
deliver(): string;
}
class Truck implements Transport {
deliver(): string {
return "Delivering by land in a truck";
}
}
class Ship implements Transport {
deliver(): string {
return "Delivering by sea in a ship";
}
}
abstract class Logistics {
abstract createTransport(): Transport;
planDelivery(): string {
const transport = this.createTransport();
return transport.deliver();
}
}
class RoadLogistics extends Logistics {
createTransport(): Transport {
return new Truck();
}
}
class SeaLogistics extends Logistics {
createTransport(): Transport {
return new Ship();
}
}
const logistics = new SeaLogistics();
console.log(logistics.planDelivery()); // "Delivering by sea in a ship"
Abstract Factory¶
Provides an interface for creating families of related objects without specifying their concrete classes. Use when your system needs to work with multiple families of products that must be used together — e.g., UI components for different operating systems.
Analogy
Like choosing between Apple and Samsung ecosystems — pick Apple and your phone, tablet, watch, and earbuds all work together seamlessly. Pick Samsung, same deal, different family. You never mix a Samsung watch with an Apple phone because the factory guarantees compatibility within its product family. Cross-platform UI frameworks face exactly this problem: one factory for iOS components, another for Android.
When to Use¶
- Your system needs families of related objects that must be used together — e.g., cross-platform UI toolkits producing matching buttons, inputs, and dialogs per OS
- Mixing products from different families would cause bugs — e.g., a Windows scrollbar inside a macOS dialog
- You want to swap entire product families at runtime or via config — e.g., switching between dark/light theme component sets, or AWS/GCP/Azure cloud SDK families
Trade-offs¶
- ✅ Guarantees compatibility within a product family — impossible to mix mismatched components
- ✅ Swapping families is a one-line change — replace the factory, all products follow
- ⚠️ Adding a new product to the family requires changing every factory. Acceptable when families are stable and rarely gain new product types, painful in rapidly evolving APIs.
- ⚠️ Significant boilerplate — interfaces + concrete classes per family. Justified when the cost of mixing incompatible products is higher than the cost of the abstraction (e.g., database provider families, cloud SDK abstractions).
Fitness Test
"Do I have families of related objects that must stay consistent — and would mixing them cause bugs?"
- Yes → ✅ Abstract Factory fits
- No, objects are independent → ❌ Factory Method is simpler
flowchart TB
A["UIFactory"] -->|createButton| B["Button"]
A -->|createCheckbox| C["Checkbox"]
D[WindowsFactory] -.->|implements| A
E[MacFactory] -.->|implements| A
D -->|creates| F[WinButton]
D -->|creates| G[WinCheckbox]
E -->|creates| H[MacButton]
E -->|creates| I[MacCheckbox]
F -.->|implements| B
H -.->|implements| B
G -.->|implements| C
I -.->|implements| C
interface Button {
render(): string;
}
interface Checkbox {
toggle(): string;
}
// Family 1: Windows
class WindowsButton implements Button {
render(): string { return "Windows button"; }
}
class WindowsCheckbox implements Checkbox {
toggle(): string { return "Windows checkbox"; }
}
// Family 2: macOS
class MacButton implements Button {
render(): string { return "macOS button"; }
}
class MacCheckbox implements Checkbox {
toggle(): string { return "macOS checkbox"; }
}
interface UIFactory {
createButton(): Button;
createCheckbox(): Checkbox;
}
class WindowsFactory implements UIFactory {
createButton(): Button { return new WindowsButton(); }
createCheckbox(): Checkbox { return new WindowsCheckbox(); }
}
class MacFactory implements UIFactory {
createButton(): Button { return new MacButton(); }
createCheckbox(): Checkbox { return new MacCheckbox(); }
}
function renderUI(factory: UIFactory): void {
const button = factory.createButton();
const checkbox = factory.createCheckbox();
console.log(button.render(), checkbox.toggle());
}
renderUI(new MacFactory()); // "macOS button" "macOS checkbox"
Builder¶
Separates the construction of a complex object from its representation, allowing the same construction process to create different representations. Use when creating objects that require many optional parameters or multi-step initialization.
Analogy
Like ordering at Subway vs. McDonald's. At McDonald's, you pick a preset meal (that's a Factory). At Subway, the sandwich artist walks you through bread, protein, veggies, sauce — step by step, same process, wildly different results. That's why SQL query builders and HTTP request builders exist: too many optional parts to shove into one constructor call.
When to Use¶
- Object construction requires many optional parameters — e.g., SQL query builders chaining
where(),orderBy(),limit() - You need step-by-step construction with validation — e.g., HTTP request builders assembling method, headers, body, and auth incrementally
- The same construction process should create different representations — e.g., a report builder producing PDF, HTML, or CSV from the same steps
Trade-offs¶
- ✅ Eliminates telescoping constructors — readable, self-documenting creation code
- ✅ Can enforce construction invariants — validate at
build()time rather than after - ⚠️ More code than a simple constructor. Acceptable when the object has 4+ optional fields or complex validation; overkill for 2–3 required fields.
- ⚠️ Mutable builder state until
build()is called. In production, make builders single-use to avoid accidental state leakage between builds.
Fitness Test
"Does my constructor have 4+ optional parameters, or does construction require multiple validated steps?"
- Yes → ✅ Builder fits
- No, just 2–3 required fields → ❌ A plain constructor is clearer
flowchart LR
A[Director] -->|directs| B[Builder]
B -->|from| B
B -->|where| B
B -->|orderBy| B
B -->|build| C[Product]
class QueryBuilder {
private table = "";
private conditions: string[] = [];
private orderByField = "";
private limitCount = 0;
from(table: string): this {
this.table = table;
return this;
}
where(condition: string): this {
this.conditions.push(condition);
return this;
}
orderBy(field: string): this {
this.orderByField = field;
return this;
}
limit(count: number): this {
this.limitCount = count;
return this;
}
build(): string {
let query = `SELECT * FROM ${this.table}`;
if (this.conditions.length) {
query += ` WHERE ${this.conditions.join(" AND ")}`;
}
if (this.orderByField) {
query += ` ORDER BY ${this.orderByField}`;
}
if (this.limitCount) {
query += ` LIMIT ${this.limitCount}`;
}
return query;
}
}
const query = new QueryBuilder()
.from("users")
.where("age > 18")
.where("status = 'active'")
.orderBy("name")
.limit(10)
.build();
console.log(query);
// SELECT * FROM users WHERE age > 18 AND status = 'active' ORDER BY name LIMIT 10
Prototype¶
Creates new objects by cloning an existing instance rather than building from scratch. Use when object creation is expensive (e.g., involves database queries or complex computation) and you need many similar objects.
Analogy
Like a spreadsheet template — instead of building a quarterly report from scratch each time, you duplicate last quarter's template and update the numbers. The clone carries over all the formatting, formulas, and structure. JavaScript's prototype chain works exactly this way: objects inherit by cloning a prototype, not by instantiating a class blueprint.
When to Use¶
- Object creation is expensive and you need many similar copies — e.g., cloning a fully-configured game entity prototype to spawn 50 enemies
- You want to avoid subclassing just to configure different initial states — e.g., cloning a "production" server config to create "staging" with a different hostname
- Pre-built templates should be duplicated and tweaked — e.g., spreadsheet templates duplicated each quarter, test data factories
Trade-offs¶
- ✅ Avoids costly re-initialization — clone carries over expensive setup
- ✅ Add new "types" at runtime by registering new prototypes — no new classes needed
- ⚠️ Deep cloning complex object graphs is tricky — circular references, shared resources. In production, define explicit
clone()methods rather than relying on generic deep-copy utilities. - ⚠️ Cloned objects are fully independent — mutations to the prototype don't propagate. Acceptable when each clone is independent; problematic if you need coordinated updates (consider Flyweight instead).
Fitness Test
"Is creating this object expensive, and do I need many similar copies with small variations?"
- Yes → ✅ Prototype fits
- No, construction is cheap → ❌ Just use
new
flowchart LR
A[Original] -- "clone()" --> B["Clone (copy)"]
B -.->|same type| A
interface Cloneable {
clone(): this;
}
class Shape implements Cloneable {
constructor(
public x: number,
public y: number,
public color: string
) {}
clone(): this {
return Object.create(
Object.getPrototypeOf(this),
Object.getOwnPropertyDescriptors(this)
);
}
}
const original = new Shape(10, 20, "red");
const copy = original.clone();
copy.color = "blue";
console.log(original.color); // "red"
console.log(copy.color); // "blue"
console.log(copy instanceof Shape); // true