docs/concepts/design-patterns.mdx
Ever find yourself solving the same problem over and over? What if experienced developers already figured out the best solutions to these recurring challenges?
// The Observer pattern — notify multiple listeners when something happens
const newsletter = {
subscribers: [],
subscribe(callback) {
this.subscribers.push(callback)
},
publish(article) {
this.subscribers.forEach(callback => callback(article))
}
}
// Anyone can subscribe
newsletter.subscribe(article => console.log(`New article: ${article}`))
newsletter.subscribe(article => console.log(`Saving "${article}" for later`))
// When we publish, all subscribers get notified
newsletter.publish("Design Patterns in JavaScript")
// "New article: Design Patterns in JavaScript"
// "Saving "Design Patterns in JavaScript" for later"
Design patterns are proven solutions to common problems in software design. They're not code you copy-paste. They're templates, blueprints, or recipes that you adapt to solve specific problems in your own code. Learning patterns gives you a vocabulary to discuss solutions with other developers and helps you recognize when a well-known solution fits your problem.
<Info> **What you'll learn in this guide:** - What design patterns are and why they matter - The Module pattern for organizing code with private state - The Singleton pattern (and why it's often unnecessary in JavaScript) - The Factory pattern for creating objects dynamically - The Observer pattern for event-driven programming - The Proxy pattern for controlling object access - The Decorator pattern for adding behavior without modification - How to choose the right pattern for your problem </Info> <Warning> **Prerequisites:** This guide assumes you understand [Factories and Classes](/concepts/factories-classes) and [IIFE, Modules & Namespaces](/concepts/iife-modules). Design patterns build on these object-oriented and modular programming concepts. </Warning>Think of design patterns like specialized tools in a toolkit. A general-purpose hammer works for many tasks, but sometimes you need a specific tool: a Phillips screwdriver for certain screws, a wrench for bolts, or pliers for gripping.
┌─────────────────────────────────────────────────────────────────────────┐
│ DESIGN PATTERNS TOOLKIT │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ CREATIONAL STRUCTURAL BEHAVIORAL │
│ ─────────── ────────── ────────── │
│ How objects How objects How objects │
│ are created are composed communicate │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Singleton │ │ Proxy │ │ Observer │ │
│ │ Factory │ │ Decorator │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ Use when you need Use when you need Use when objects │
│ to control object to wrap or extend need to react to │
│ creation objects changes in others │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ MODULE (JS-specific) — Encapsulates code with private state │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
You don't use every tool for every job. Similarly, you don't use every pattern in every project. The skill is recognizing when a pattern fits your problem.
Design patterns are typical solutions to commonly occurring problems in software design. The term was popularized by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — the "Gang of Four" (GoF) — in their 1994 book Design Patterns: Elements of Reusable Object-Oriented Software. They catalogued 23 patterns that developers kept reinventing, and the book has sold over 500,000 copies worldwide.
The original GoF patterns were written for languages like C++ and Smalltalk. As Addy Osmani explains in Learning JavaScript Design Patterns, JavaScript is fundamentally different:
| Feature | Impact on Patterns |
|---|---|
| First-class functions | Many patterns simplify to just passing functions around |
| Prototypal inheritance | No need for complex class hierarchies |
| ES Modules | Built-in module system replaces manual Module pattern |
| Dynamic typing | No need for interface abstractions |
| Closures | Natural way to create private state |
This means some classical patterns are overkill in JavaScript, while others become more elegant. We'll focus on the patterns that are genuinely useful in modern JavaScript.
The original GoF patterns are grouped into three categories:
Creational Patterns — Control how objects are created
Structural Patterns — Control how objects are composed
Behavioral Patterns — Control how objects communicate
We'll cover six patterns that are particularly useful in JavaScript: Module (JS-specific), Singleton, Factory, Observer, Proxy, and Decorator.
The Module pattern encapsulates code into reusable units with private and public parts. Before ES6 modules existed, developers used IIFEs (Immediately Invoked Function Expressions) to create this pattern. Today, JavaScript has built-in ES Modules that provide this naturally.
Each file is its own module. Variables are private unless you export them:
// counter.js — A module with private state
let count = 0 // Private — not exported, not accessible outside
export function increment() {
count++
return count
}
export function decrement() {
count--
return count
}
export function getCount() {
return count
}
// main.js — Using the module
import { increment, getCount } from './counter.js'
increment()
increment()
console.log(getCount()) // 2
// Trying to access private state
// console.log(count) // ReferenceError: count is not defined
Before ES6, developers used closures to create modules:
// The revealing module pattern using IIFE
const Counter = (function() {
// Private variables and functions
let count = 0
function logChange(action) {
console.log(`Counter ${action}: ${count}`)
}
// Public API — "revealed" by returning an object
return {
increment() {
count++
logChange('incremented')
return count
},
decrement() {
count--
logChange('decremented')
return count
},
getCount() {
return count
}
}
})()
Counter.increment() // "Counter incremented: 1"
Counter.increment() // "Counter incremented: 2"
console.log(Counter.getCount()) // 2
// Private members are truly private
console.log(Counter.count) // undefined
console.log(Counter.logChange) // undefined
The Singleton pattern ensures a class has only one instance and provides a global access point to that instance. According to Refactoring Guru, it solves two problems: guaranteeing a single instance and providing global access to it.
// Singleton using Object.freeze — immutable configuration
const Config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
debug: false
}
Object.freeze(Config) // Prevent all modifications
// Usage anywhere in your app
console.log(Config.apiUrl) // "https://api.example.com"
// Attempting to modify throws an error in strict mode (silently fails otherwise)
Config.apiUrl = 'https://evil.com'
console.log(Config.apiUrl) // Still "https://api.example.com"
Config.debug = true
console.log(Config.debug) // Still false — frozen objects are immutable
let instance = null
class Database {
constructor() {
if (instance) {
return instance // Return existing instance
}
this.connection = null
instance = this
}
connect(url) {
if (!this.connection) {
this.connection = `Connected to ${url}`
console.log(this.connection)
}
return this.connection
}
}
const db1 = new Database()
const db2 = new Database()
console.log(db1 === db2) // true — Same instance!
db1.connect('mongodb://localhost') // "Connected to mongodb://localhost"
db2.connect('mongodb://other') // Returns same connection, doesn't reconnect
Here's the thing: Singletons are often unnecessary in JavaScript. Here's why:
// ES Modules are already singletons!
// config.js
export const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
}
// main.js
import { config } from './config.js'
// other.js
import { config } from './config.js'
// Both files get the SAME object — modules are cached!
Better alternatives: Dependency injection, React Context, or simply exporting an object from a module. </Warning>
Despite the caveats, Singletons can be appropriate for:
The Factory pattern creates objects without exposing the creation logic. Instead of using new directly, you call a factory function that returns the appropriate object. According to the State of JS 2023 survey, factory functions are among the most commonly used patterns in modern JavaScript codebases. This centralizes object creation and makes it easy to change how objects are created without updating every call site.
// Factory function — creates different user types
function createUser(type, name) {
const baseUser = {
name,
createdAt: new Date(),
greet() {
return `Hi, I'm ${this.name}`
}
}
switch (type) {
case 'admin':
return {
...baseUser,
role: 'admin',
permissions: ['read', 'write', 'delete', 'manage-users'],
promote(user) {
console.log(`${this.name} promoted ${user.name}`)
}
}
case 'editor':
return {
...baseUser,
role: 'editor',
permissions: ['read', 'write']
}
case 'viewer':
default:
return {
...baseUser,
role: 'viewer',
permissions: ['read']
}
}
}
// Usage — no need to know the internal structure
const admin = createUser('admin', 'Alice')
const editor = createUser('editor', 'Bob')
const viewer = createUser('viewer', 'Charlie')
console.log(admin.permissions) // ['read', 'write', 'delete', 'manage-users']
console.log(editor.permissions) // ['read', 'write']
console.log(viewer.greet()) // "Hi, I'm Charlie"
The Observer pattern defines a subscription mechanism that notifies multiple objects about events. According to Refactoring Guru, it lets you "define a subscription mechanism to notify multiple objects about any events that happen to the object they're observing."
This pattern is everywhere: DOM events, React state updates, Redux subscriptions, Node.js EventEmitter, and RxJS observables all use variations of Observer.
class Observable {
constructor() {
this.observers = []
}
subscribe(fn) {
this.observers.push(fn)
// Return an unsubscribe function
return () => {
this.observers = this.observers.filter(observer => observer !== fn)
}
}
notify(data) {
this.observers.forEach(observer => observer(data))
}
}
// Usage: A stock price tracker
const stockPrice = new Observable()
// Subscriber 1: Log to console
const unsubscribeLogger = stockPrice.subscribe(price => {
console.log(`Stock price updated: $${price}`)
})
// Subscriber 2: Check for alerts
stockPrice.subscribe(price => {
if (price > 150) {
console.log('ALERT: Price above $150!')
}
})
// Subscriber 3: Update UI (simulated)
stockPrice.subscribe(price => {
console.log(`Updating chart with price: $${price}`)
})
// When price changes, all subscribers are notified
stockPrice.notify(145)
// "Stock price updated: $145"
// "Updating chart with price: $145"
stockPrice.notify(155)
// "Stock price updated: $155"
// "ALERT: Price above $150!"
// "Updating chart with price: $155"
// Unsubscribe the logger
unsubscribeLogger()
stockPrice.notify(160)
// No log message, but alert and chart still update
┌─────────────────────────────────────────────────────────────────────────┐
│ THE OBSERVER PATTERN │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ PUBLISHER (Observable) SUBSCRIBERS (Observers) │
│ ────────────────────── ─────────────────────── │
│ │
│ ┌─────────────────────┐ ┌─────────────┐ │
│ │ │ ──────────► │ Reader #1 │ │
│ │ Magazine │ └─────────────┘ │
│ │ Publisher │ ┌─────────────┐ │
│ │ │ ──────────► │ Reader #2 │ │
│ │ • subscribers[] │ └─────────────┘ │
│ │ • subscribe() │ ┌─────────────┐ │
│ │ • unsubscribe() │ ──────────► │ Reader #3 │ │
│ │ • notify() │ └─────────────┘ │
│ │ │ │
│ └─────────────────────┘ │
│ │
│ When a new issue publishes, all subscribers receive it automatically. │
│ Readers can subscribe or unsubscribe at any time. │
│ │
└─────────────────────────────────────────────────────────────────────────┘
// Observable form field
class FormField {
constructor(initialValue = '') {
this.value = initialValue
this.observers = []
}
subscribe(fn) {
this.observers.push(fn)
return () => {
this.observers = this.observers.filter(o => o !== fn)
}
}
setValue(newValue) {
this.value = newValue
this.observers.forEach(fn => fn(newValue))
}
}
// Usage
const emailField = new FormField('')
// Validator subscriber
emailField.subscribe(value => {
const isValid = value.includes('@')
console.log(isValid ? 'Valid email' : 'Invalid email')
})
// Character counter subscriber
emailField.subscribe(value => {
console.log(`Characters: ${value.length}`)
})
emailField.setValue('test')
// "Invalid email"
// "Characters: 4"
emailField.setValue('[email protected]')
// "Valid email"
// "Characters: 16"
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. In JavaScript, the ES6 Proxy object lets you intercept and redefine fundamental operations like property access, assignment, and function calls.
const user = {
name: 'Alice',
age: 25,
email: '[email protected]'
}
const userProxy = new Proxy(user, {
// Intercept property reads
get(target, property) {
console.log(`Accessing property: ${property}`)
return target[property]
},
// Intercept property writes
set(target, property, value) {
console.log(`Setting ${property} to ${value}`)
// Validation: age must be a non-negative number
if (property === 'age') {
if (typeof value !== 'number' || value < 0) {
throw new Error('Age must be a non-negative number')
}
}
// Validation: email must contain @
if (property === 'email') {
if (!value.includes('@')) {
throw new Error('Invalid email format')
}
}
target[property] = value
return true
}
})
// All access goes through the proxy
console.log(userProxy.name)
// "Accessing property: name"
// "Alice"
userProxy.age = 26
// "Setting age to 26"
userProxy.age = -5
// Error: Age must be a non-negative number
userProxy.email = 'invalid'
// Error: Invalid email format
// Expensive object that we don't want to create until needed
function createExpensiveResource() {
console.log('Creating expensive resource...')
return {
data: 'Loaded data from database',
process() {
return `Processing: ${this.data}`
}
}
}
// Proxy that delays creation until first use
function createLazyResource() {
let resource = null
return new Proxy({}, {
get(target, property) {
// Create resource on first access
if (!resource) {
resource = createExpensiveResource()
}
const value = resource[property]
// If it's a method, bind it to the resource
return typeof value === 'function' ? value.bind(resource) : value
}
})
}
const lazyResource = createLazyResource()
console.log('Proxy created, resource not loaded yet')
// Resource is only created when we actually use it
console.log(lazyResource.data)
// "Creating expensive resource..."
// "Loaded data from database"
console.log(lazyResource.process())
// "Processing: Loaded data from database"
| Use Case | Example |
|---|---|
| Validation | Validate data before setting properties |
| Logging/Debugging | Log all property accesses for debugging |
| Lazy initialization | Delay expensive object creation |
| Access control | Restrict access to certain properties |
| Caching | Cache expensive computations |
The Decorator pattern attaches new behaviors to objects by wrapping them in objects that contain these behaviors. According to Refactoring Guru, it lets you "attach new behaviors to objects by placing these objects inside special wrapper objects."
In JavaScript, decorators are often implemented as functions that take an object and return an enhanced version.
// Base object
const createCharacter = (name) => ({
name,
health: 100,
describe() {
return `${this.name} (${this.health} HP)`
}
})
// Decorator: Add flying ability
const withFlying = (character) => ({
...character,
fly() {
return `${character.name} soars through the sky!`
},
describe() {
return `${character.describe()} [Can fly]`
}
})
// Decorator: Add swimming ability
const withSwimming = (character) => ({
...character,
swim() {
return `${character.name} dives into the water!`
},
describe() {
return `${character.describe()} [Can swim]`
}
})
// Decorator: Add armor
const withArmor = (character, armorPoints) => ({
...character,
armor: armorPoints,
takeDamage(amount) {
const reducedDamage = Math.max(0, amount - armorPoints)
character.health -= reducedDamage
return `${character.name} takes ${reducedDamage} damage (${armorPoints} blocked)`
},
describe() {
return `${character.describe()} [Armor: ${armorPoints}]`
}
})
// Compose decorators to build characters
const duck = withSwimming(withFlying(createCharacter('Duck')))
console.log(duck.describe()) // "Duck (100 HP) [Can fly] [Can swim]"
console.log(duck.fly()) // "Duck soars through the sky!"
console.log(duck.swim()) // "Duck dives into the water!"
const knight = withArmor(createCharacter('Knight'), 20)
console.log(knight.describe()) // "Knight (100 HP) [Armor: 20]"
console.log(knight.takeDamage(50)) // "Knight takes 30 damage (20 blocked)"
Decorators also work great with functions:
// Decorator: Log function calls
const withLogging = (fn, fnName) => {
return function(...args) {
console.log(`Calling ${fnName} with:`, args)
const result = fn.apply(this, args)
console.log(`${fnName} returned:`, result)
return result
}
}
// Decorator: Memoize (cache) results
const withMemoization = (fn) => {
const cache = new Map()
return function(...args) {
const key = JSON.stringify(args)
if (cache.has(key)) {
console.log('Cache hit!')
return cache.get(key)
}
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
// Original function
function fibonacci(n) {
if (n <= 1) return n
return fibonacci(n - 1) + fibonacci(n - 2)
}
// Decorated version with logging
const loggedAdd = withLogging((a, b) => a + b, 'add')
loggedAdd(2, 3)
// "Calling add with: [2, 3]"
// "add returned: 5"
// Decorated fibonacci with memoization
const memoizedFib = withMemoization(function fib(n) {
if (n <= 1) return n
return memoizedFib(n - 1) + memoizedFib(n - 2)
})
console.log(memoizedFib(10)) // 55
console.log(memoizedFib(10)) // "Cache hit!" — 55
┌─────────────────────────────────────────────────────────────────────────┐
│ DESIGN PATTERN MISTAKES │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ MISTAKE #1: PATTERN OVERUSE │
│ ─────────────────────────── │
│ Using patterns where simple code would work better. │
│ A plain function is often better than a Factory class. │
│ │
│ MISTAKE #2: WRONG PATTERN CHOICE │
│ ───────────────────────────── │
│ Using Singleton when you just need a module export. │
│ Using Observer when a simple callback would suffice. │
│ │
│ MISTAKE #3: IGNORING JAVASCRIPT IDIOMS │
│ ──────────────────────────────────── │
│ JavaScript has closures, first-class functions, and ES modules. │
│ Many classical patterns simplify dramatically in JavaScript. │
│ │
│ MISTAKE #4: PREMATURE ABSTRACTION │
│ ──────────────────────────────── │
│ Adding patterns before you have a real problem to solve. │
│ "You Ain't Gonna Need It" (YAGNI) applies to patterns too. │
│ │
└─────────────────────────────────────────────────────────────────────────┘
When you learn a new pattern, resist the urge to use it everywhere:
// ❌ OVERKILL: Factory for simple objects
class UserFactory {
createUser(name) {
return new User(name)
}
}
const factory = new UserFactory()
const user = factory.createUser('Alice')
// ✓ SIMPLE: Just create the object
const user = { name: 'Alice' }
// or
const user = new User('Alice')
| Problem | Pattern | Alternative |
|---|---|---|
| Need to organize code with private state | Module | ES6 module exports |
| Need exactly one instance | Singleton | Just export an object from a module |
| Need to create objects dynamically | Factory | Plain function returning objects |
| Need to notify multiple listeners of changes | Observer | EventEmitter, callbacks, or a library |
| Need to control or validate object access | Proxy | Getter/setter methods |
| Need to add behavior without modification | Decorator | Higher-order functions, composition |
Design patterns are templates, not code — Adapt them to your specific problem; don't force-fit them
JavaScript simplifies many patterns — First-class functions, closures, and ES modules reduce boilerplate
Module pattern organizes code — Use ES modules for new projects; understand IIFE pattern for legacy code
Singleton is often unnecessary — ES module exports are already cached; use sparingly if at all
Factory centralizes object creation — Great for creating different types based on input
Observer enables event-driven code — The foundation of DOM events, React state, and reactive programming
Proxy intercepts object operations — Use for validation, logging, lazy loading, and access control
Decorator adds behavior through wrapping — Compose features without modifying original code
Avoid pattern overuse — Simple code beats clever patterns; apply the YAGNI principle
Learn to recognize patterns in the wild — DOM events use Observer, Promises use a form of Observer, middleware uses Decorator
</Info>The Module pattern encapsulates code into reusable units with **private and public parts**. It allows you to:
- Hide implementation details (private variables and functions)
- Expose only a public API
- Avoid polluting the global namespace
In modern JavaScript, ES6 modules (`import`/`export`) provide this naturally. Variables in a module are private unless exported.
```javascript
// privateHelper is not exported — it's private
function privateHelper() { /* ... */ }
// Only publicFunction is accessible to importers
export function publicFunction() {
privateHelper()
}
```
Singleton is often unnecessary in JavaScript because:
1. **ES modules are already singletons** — When you export an object, all importers get the same instance
2. **Testing difficulties** — Tests share state, making isolation hard
3. **Hidden dependencies** — Code using Singletons has implicit dependencies
4. **JavaScript can create objects directly** — No need for the class-based workarounds other languages require
```javascript
// ES module — already a singleton!
export const config = { apiUrl: '...' }
// Every import gets the same object
import { config } from './config.js' // Same instance everywhere
```
The Observer pattern has three key parts:
1. **Subscriber list** — An array to store observer functions
2. **Subscribe method** — Adds a function to the list (often returns an unsubscribe function)
3. **Notify method** — Calls all subscribed functions with data
```javascript
class Observable {
constructor() {
this.observers = [] // 1. Subscriber list
}
subscribe(fn) { // 2. Subscribe method
this.observers.push(fn)
return () => { // Returns unsubscribe
this.observers = this.observers.filter(o => o !== fn)
}
}
notify(data) { // 3. Notify method
this.observers.forEach(fn => fn(data))
}
}
```
Both wrap objects, but they have different purposes:
**Proxy Pattern:**
- **Controls access** to an object
- Intercepts operations like get, set, delete
- The proxy typically has the same interface as the target
- Use for: validation, logging, lazy loading, access control
**Decorator Pattern:**
- **Adds new behavior** to an object
- Wraps the object and extends its capabilities
- May add new methods or modify existing ones
- Use for: composing features, cross-cutting concerns
```javascript
// Proxy — same interface, controlled access
const proxy = new Proxy(obj, { get(t, p) { /* intercept */ } })
// Decorator — enhanced interface, new behavior
const enhanced = withLogging(withCache(obj))
```
Use the Factory pattern when:
1. **Object creation is complex** — Encapsulate setup logic in one place
2. **You need different types based on input** — Switch logic centralized in the factory
3. **You want to decouple creation from usage** — Callers don't need to know implementation
4. **You might change how objects are created** — Update the factory, not every call site
```javascript
// Factory — creation logic in one place
function createNotification(type, message) {
switch (type) {
case 'error': return { type, message, color: 'red' }
case 'success': return { type, message, color: 'green' }
default: return { type: 'info', message, color: 'blue' }
}
}
// Easy to use — no need to know the structure
const notification = createNotification('error', 'Something went wrong')
```
The "Golden Hammer" anti-pattern is the tendency to use a familiar tool (or pattern) for every problem, even when it's not appropriate.
**Signs you're doing this:**
- Using Singleton for everything that "should be global"
- Creating Factory classes for simple object literals
- Using Observer when a callback would suffice
- Adding patterns before you have a real problem
**How to avoid it:**
- Start with the simplest solution
- Add patterns only when you hit a real problem they solve
- Ask: "Would a plain function/object work here?"
- Remember: Code clarity beats clever patterns