Creational: Factory Pattern
The Factory pattern abstracts object creation behind a single function or class. Callers request an instance by type without coupling themselves to concrete constructors — making it easy to swap implementations later.
Client calls createButton(type)
→
Factory resolves type
→
Returns concrete instance
factory.ts
// Factory Pattern
interface Button { render(): string }
class PrimaryButton implements Button {
render() { return "<button class='primary'>Click</button>" }
}
class IconButton implements Button {
render() { return "<button class='icon'>★</button>" }
}
function createButton(type: "primary" | "icon"): Button {
const map = { primary: PrimaryButton, icon: IconButton }
return new map[type]()
}
const btn = createButton("primary")
btn.render() // → <button class='primary'>Click</button>Behavioral: Observer Pattern
The Observer pattern lets a subject notify many subscribers when its state changes. It is the foundation of event systems, reactive stores, and real-time UIs. Each subscriber can also unsubscribe independently.
Subject
→
emit(data)
→
Observer 1
→
Observer 2
Observer Message Flow
1
State Changes
Subject state mutates
2
emit(data)
Notifies all listeners
3
Observer 1
Receives notification
4
Observer 2
Receives notification
5
Unsubscribe
Listeners detach cleanly
observer.ts
// Observer Pattern
type Listener<T> = (data: T) => void
class EventEmitter<T> {
private listeners: Listener<T>[] = []
subscribe(fn: Listener<T>) {
this.listeners.push(fn)
// returns an unsubscribe function
return () => { this.listeners = this.listeners.filter(l => l !== fn) }
}
emit(data: T) {
this.listeners.forEach(fn => fn(data))
}
}
const counter = new EventEmitter<number>()
const unsub = counter.subscribe(v => console.log("Observer 1:", v))
counter.subscribe(v => console.log("Observer 2:", v))
counter.emit(42) // both observers fire
unsub() // Observer 1 is now unsubscribedStructural: Strategy Pattern
The Strategy pattern defines a family of interchangeable algorithms behind a common interface. You swap behavior at runtime without touching the host class — eliminating chains of if/else or switch statements.
Interchangeable Strategies
ascending: SortStrategy
descending: SortStrategy
mergeSort: SortStrategy
strategy.ts
// Strategy Pattern
interface SortStrategy {
sort(data: number[]): number[]
}
const ascending: SortStrategy = {
sort: (data) => [...data].sort((a, b) => a - b),
}
const descending: SortStrategy = {
sort: (data) => [...data].sort((a, b) => b - a),
}
class Sorter {
constructor(private strategy: SortStrategy) {}
setStrategy(s: SortStrategy) { this.strategy = s }
sort(data: number[]) { return this.strategy.sort(data) }
}
const sorter = new Sorter(ascending)
sorter.sort([3, 1, 4, 1, 5]) // [1, 1, 3, 4, 5]
sorter.setStrategy(descending) // swap at runtime
sorter.sort([3, 1, 4, 1, 5]) // [5, 4, 3, 1, 1]When to Use What
| Pattern | Use Case |
|---|---|
| Factory | Multiple object types sharing one interface — UI components, parsers, adapters |
| Observer | One-to-many state updates — event buses, reactive stores, WebSocket feeds |
| Strategy | Swappable algorithms — sorting, validation rules, payment processors |
When to Apply Each Pattern
Simple app
Start without patterns — plain functions and objects
Multiple object types
Reach for Factory to unify creation logic
Growing event complexity
Observer decouples state updates from consumers
Diverging algorithms
Strategy swaps behavior without branching
Anti-patterns to Avoid
!God Object
A class with 20+ unrelated methods becomes untestable and fragile. Split responsibilities across focused classes instead of accumulating them in one place.
!Premature Abstraction
Do not apply patterns speculatively. Add a Factory only when you actually have two or more concrete types to manage. Add Observer only when real subscribers exist.
!Pattern Overuse
Every pattern adds a layer of indirection. If the abstraction does not simplify your code or tests, it is adding complexity, not reducing it. Start simple and refactor toward patterns only when the need is clear.