Learn JavaScript design patterns like Module, Singleton, Observer, Factory, Proxy, and Decorator. Understand when to use each pattern and avoid common pitfalls.
Ever find yourself solving the same problem over and over? What if experienced developers already figured out the best solutions to these recurring challenges?
Copy
Ask AI
// The Observer pattern — notify multiple listeners when something happensconst newsletter = { subscribers: [], subscribe(callback) { this.subscribers.push(callback) }, publish(article) { this.subscribers.forEach(callback => callback(article)) }}// Anyone can subscribenewsletter.subscribe(article => console.log(`New article: ${article}`))newsletter.subscribe(article => console.log(`Saving "${article}" for later`))// When we publish, all subscribers get notifiednewsletter.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.
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
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.
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────────┐│ 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.
Behavioral Patterns — Control how objects communicate
Observer, Strategy, Command, Mediator, Iterator, State, and others
JavaScript-specific patterns: The Module pattern isn’t one of the original 23 GoF patterns. It’s a JavaScript idiom that emerged to solve JavaScript-specific problems (like the lack of built-in modules before ES6). We include it here because it’s essential for JavaScript developers.
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:
Copy
Ask AI
// counter.js — A module with private statelet count = 0 // Private — not exported, not accessible outsideexport function increment() { count++ return count}export function decrement() { count-- return count}export function getCount() { return count}// main.js — Using the moduleimport { increment, getCount } from './counter.js'increment()increment()console.log(getCount()) // 2// Trying to access private state// console.log(count) // ReferenceError: count is not defined
Group related functions and data together. A UserService module might contain login(), logout(), getCurrentUser(), and private token storage.
Hiding implementation details
Expose only what consumers need. Internal helper functions, validation logic, and caching mechanisms stay private.
Avoiding global namespace pollution
Instead of 50 global functions, you have one module export. This prevents naming collisions with other code.
Modern JavaScript: Use ES6 modules (import/export) for new projects. The IIFE pattern is mainly for legacy code or environments without module support. See IIFE, Modules & Namespaces for a deeper dive.
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.
Why Singleton Is Often an Anti-Pattern in JavaScript
Here’s the thing: Singletons are often unnecessary in JavaScript. Here’s why:
Copy
Ask AI
// ES Modules are already singletons!// config.jsexport const config = { apiUrl: 'https://api.example.com', timeout: 5000}// main.jsimport { config } from './config.js'// other.jsimport { config } from './config.js'// Both files get the SAME object — modules are cached!
Problems with Singletons:
Testing difficulties — Tests share the same instance, making isolation hard
Hidden dependencies — Code that uses a Singleton has an implicit dependency
Tight coupling — Components become coupled to a specific implementation
ES Modules already do this — Module exports are cached; you get the same object every time
Better alternatives: Dependency injection, React Context, or simply exporting an object from a module.
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.
Copy
Ask AI
// Factory function — creates different user typesfunction 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 structureconst 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"
Creating objects with complex setup — Encapsulate the complexity
Creating different types based on input — Switch logic in one place
Decoupling creation from usage — Callers don’t need to know implementation details
Want to go deeper? The Factory pattern is covered extensively in Factories and Classes, including factory functions vs classes, the new keyword, and when to use each approach.
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.
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 proxyconsole.log(userProxy.name)// "Accessing property: name"// "Alice"userProxy.age = 26// "Setting age to 26"userProxy.age = -5// Error: Age must be a non-negative numberuserProxy.email = 'invalid'// Error: Invalid email format
// Expensive object that we don't want to create until neededfunction createExpensiveResource() { console.log('Creating expensive resource...') return { data: 'Loaded data from database', process() { return `Processing: ${this.data}` } }}// Proxy that delays creation until first usefunction 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 itconsole.log(lazyResource.data)// "Creating expensive resource..."// "Loaded data from database"console.log(lazyResource.process())// "Processing: Loaded data from database"
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.
┌─────────────────────────────────────────────────────────────────────────┐│ 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:
Copy
Ask AI
// ❌ OVERKILL: Factory for simple objectsclass UserFactory { createUser(name) { return new User(name) }}const factory = new UserFactory()const user = factory.createUser('Alice')// ✓ SIMPLE: Just create the objectconst user = { name: 'Alice' }// orconst user = new User('Alice')
Ask yourself these questions before using a pattern:
Do I have a real problem? Don’t solve problems you don’t have yet.
Is there a simpler solution? A plain function or object might be enough.
Does JavaScript already solve this? ES modules, Promises, and iterators are built-in patterns.
Will my team understand it? Patterns only help if everyone knows them.
Question 1: What's the main purpose of the Module pattern?
Answer: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.
Copy
Ask AI
// privateHelper is not exported — it's privatefunction privateHelper() { /* ... */ }// Only publicFunction is accessible to importersexport function publicFunction() { privateHelper()}
Question 2: Why is Singleton often considered an anti-pattern in JavaScript?
Answer:Singleton is often unnecessary in JavaScript because:
ES modules are already singletons — When you export an object, all importers get the same instance
Testing difficulties — Tests share state, making isolation hard
Hidden dependencies — Code using Singletons has implicit dependencies
JavaScript can create objects directly — No need for the class-based workarounds other languages require
Copy
Ask AI
// ES module — already a singleton!export const config = { apiUrl: '...' }// Every import gets the same objectimport { config } from './config.js' // Same instance everywhere
Question 3: What are the three parts of an Observer pattern implementation?
Answer:The Observer pattern has three key parts:
Subscriber list — An array to store observer functions
Subscribe method — Adds a function to the list (often returns an unsubscribe function)
Notify method — Calls all subscribed functions with data
Question 4: How does the Proxy pattern differ from the Decorator pattern?
Answer: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
Copy
Ask AI
// Proxy — same interface, controlled accessconst proxy = new Proxy(obj, { get(t, p) { /* intercept */ } })// Decorator — enhanced interface, new behaviorconst enhanced = withLogging(withCache(obj))
Question 5: When should you use the Factory pattern?
Answer:Use the Factory pattern when:
Object creation is complex — Encapsulate setup logic in one place
You need different types based on input — Switch logic centralized in the factory
You want to decouple creation from usage — Callers don’t need to know implementation
You might change how objects are created — Update the factory, not every call site
Copy
Ask AI
// Factory — creation logic in one placefunction 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 structureconst notification = createNotification('error', 'Something went wrong')
Question 6: What's the 'Golden Hammer' anti-pattern?
Answer: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
Design patterns are reusable solutions to common problems in software design. They are not specific code implementations but templates you adapt to your needs. In JavaScript, many classical patterns simplify because the language has first-class functions, closures, and prototypal inheritance, which reduce the need for complex class hierarchies.
What is the Observer pattern and when should I use it?
The Observer pattern lets an object (the subject) notify a list of dependents (observers) when its state changes. Use it when multiple parts of your application need to react to the same event. The DOM’s addEventListener is a built-in Observer implementation, and frameworks like React and Vue use Observer-like patterns internally for reactivity.
Are all Gang of Four design patterns relevant in JavaScript?
No. As Addy Osmani notes in Learning JavaScript Design Patterns, many GoF patterns were designed for statically typed languages like C++ and Java. JavaScript’s first-class functions, closures, and dynamic typing make patterns like Strategy, Command, and Iterator trivial — often just a function or callback. Focus on patterns that solve real problems in your codebase.
Why is the Singleton pattern considered an anti-pattern in JavaScript?
ES Modules are already singletons — a module’s exports are cached after the first import, so every file gets the same object. Using a Singleton class adds complexity without benefit. Singletons also make testing harder because components share hidden global state. Prefer dependency injection or module exports instead.
What is the difference between the Proxy and Decorator patterns?
The Proxy pattern controls access to an object without changing its interface — it intercepts operations like property reads or function calls. The Decorator pattern adds new behavior or capabilities to an object by wrapping it. JavaScript’s built-in Proxy object implements the Proxy pattern natively since ES2015.