Design patterns are solutions to recurring software engineering problems, not rules to follow blindly. The Gang of Four catalogued 23 patterns in 1994. Most JavaScript developers use a subset daily without naming them. Naming and understanding them makes you more effective at recognizing problems and communicating solutions with other engineers. This guide focuses on JavaScript design patterns, with the production trade-offs senior engineers need to make sound decisions.
⚡ TL;DR: Creational patterns (Singleton, Factory) create objects. Structural patterns (Proxy, Decorator, Module) compose objects. Behavioral patterns (Observer, Strategy, Command) define object interaction. JavaScript’s dynamic nature makes many patterns simpler than their Java/C++ implementations.
Singleton — one instance only — JavaScript design patterns
// Singleton: ensure only one instance exists globally
// Modern JavaScript: ES modules are singletons by default!
// db.js
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export default pool; // Imported anywhere: always same pool instance
// The classic class-based singleton:
class Logger {
static instance;
constructor() {
if (Logger.instance) return Logger.instance;
Logger.instance = this;
this.logs = [];
}
log(msg) { this.logs.push({ msg, at: Date.now() }); }
}
const a = new Logger();
const b = new Logger();
a === b; // true — same instance
// When to use: shared resource (DB pool, logger, config)
// When NOT to use: when it creates hidden global state that makes testing hardFactory — create objects without specifying class
// Factory: centralize object creation logic
class NotificationFactory {
static create(type, payload) {
switch (type) {
case 'email': return new EmailNotification(payload);
case 'sms': return new SMSNotification(payload);
case 'push': return new PushNotification(payload);
case 'webhook': return new WebhookNotification(payload);
default: throw new Error(`Unknown notification type: ${type}`);
}
}
}
// Usage: caller doesn't know which class is instantiated
const notif = NotificationFactory.create('email', { to: 'alice@example.com', body: '...' });
await notif.send();
// Abstract Factory: factory of factories
// Strategy: select algorithm at runtime (see below)Observer — event-driven decoupling
// Observer: objects subscribe to events from a subject
// JavaScript native: EventEmitter (Node.js), EventTarget (browser)
class EventEmitter {
#listeners = new Map();
on(event, handler) {
if (!this.#listeners.has(event)) this.#listeners.set(event, new Set());
this.#listeners.get(event).add(handler);
return () => this.off(event, handler); // Returns unsubscribe function
}
off(event, handler) {
this.#listeners.get(event)?.delete(handler);
}
emit(event, ...args) {
this.#listeners.get(event)?.forEach(h => h(...args));
}
}
const store = new EventEmitter();
const unsubscribe = store.on('user:updated', (user) => updateUI(user));
// Later: unsubscribe() to avoid memory leaksStrategy — swap algorithms at runtime
// Strategy: define family of algorithms, make them interchangeable
// Instead of:
function sort(arr, type) {
if (type === 'bubble') { /* ... */ }
else if (type === 'quick') { /* ... */ }
// Growing if/else = code smell
}
// Strategy pattern:
const sortStrategies = {
bubble: (arr) => { /* bubble sort */ },
quick: (arr) => { /* quick sort */ },
merge: (arr) => { /* merge sort */ },
};
class Sorter {
#strategy;
constructor(strategy) { this.#strategy = strategy; }
setStrategy(strategy) { this.#strategy = strategy; } // Swap at runtime
sort(data) { return this.#strategy(data); }
}
const sorter = new Sorter(sortStrategies.quick);
sorter.sort(data);
sorter.setStrategy(sortStrategies.merge); // Change algorithm without changing codeCommand — encapsulate operations for undo/redo
// Command: encapsulate action as object — enables undo/redo, queuing, logging
class CommandHistory {
#history = [];
#redoStack = [];
execute(command) {
command.execute();
this.#history.push(command);
this.#redoStack = []; // Clear redo on new action
}
undo() {
const command = this.#history.pop();
if (command) { command.undo(); this.#redoStack.push(command); }
}
redo() {
const command = this.#redoStack.pop();
if (command) { command.execute(); this.#history.push(command); }
}
}
// Each action is a command:
class UpdateTextCommand {
constructor(editor, newText) {
this.editor = editor;
this.newText = newText;
this.oldText = editor.getText(); // Save for undo
}
execute() { this.editor.setText(this.newText); }
undo() { this.editor.setText(this.oldText); }
}Design patterns quick reference
- Singleton: global shared resource. JS modules are singletons naturally.
- Factory: centralized object creation. Replaces type-switching new calls.
- Observer: event-driven communication. Node EventEmitter, DOM events.
- Strategy: swappable algorithms. Replaces if/else or switch type checks.
- Command: encapsulate actions. Enables undo/redo, queuing, audit logs.
- Proxy: intercept operations. ES6 Proxy for validation, logging, lazy loading.
- Decorator: add behavior without subclassing. Function wrappers, middleware.
- Module: encapsulate private state. ES6 modules + closure pattern.
The Proxy pattern in JavaScript is covered in depth in the JavaScript Proxy deep dive. The Observer pattern powers the reactive systems described in the React hooks guide. External reference: Refactoring Guru — Design Patterns in JavaScript.
Recommended Reading
→ Designing Data-Intensive Applications — The essential book every senior developer needs. Covers distributed systems, databases, and production architecture.
→ The Pragmatic Programmer — Timeless engineering wisdom for writing better, more maintainable code at any level.
Affiliate links. We earn a small commission at no extra cost to you.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
Join 1,000+ senior developers getting expert-level JS, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.
Related: TypeScript for JavaScript Developers: The Migration Guide That Actually Works · JavaScript Closures, Scope, and the Module Pattern: The Complete Guide · JavaScript Promises: The Complete Guide From Basics to Advanced Patterns
Related: JavaScript Async/Await: Every Pattern, Pitfall, and Performance Trick · JavaScript Array Methods: Every Method Explained With Real Examples · DynamoDB Single-Table Design: The Pattern That Scales to Billions of Items
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
