Design Patterns in JavaScript — The Ones That Actually Show Up in Real Code
Forget the Gang of Four textbook. These are the patterns I see in production JavaScript and TypeScript codebases every week — observer, factory, strategy, and the ones nobody names but everyone uses.

Read the Gang of Four book in college. Understood maybe 30% of it, if I'm being honest. The examples were in C++ and Smalltalk. The problems they solved — creating families of related objects in a way that's independent of the concrete classes — sounded important in the abstract but had no connection to anything I was building. I could recite the names: Abstract Factory, Bridge, Flyweight, Chain of Responsibility. Couldn't apply any of them to the Express API I was working on that weekend.
Years later, I realized I'd probably been using design patterns all along without knowing their names. Every event listener is the Observer pattern. Every Express middleware is Chain of Responsibility. Every time I wrote a function that returned different objects based on a type parameter, that was Factory. The patterns weren't academic abstractions — they were descriptions of things JavaScript developers do naturally.
The disconnect was the textbook presentation. Patterns described in terms of abstract classes, interfaces, and inheritance hierarchies don't seem to map well to a language where functions are first-class, objects are created with literals, and inheritance is the exception rather than the rule. Once I started seeing patterns through a JavaScript lens, they went from interview trivia to genuinely useful vocabulary for talking about code structure.
Observer Pattern — You Already Use This
The Observer pattern lets an object (the subject) maintain a list of dependents (observers) and notify them when state changes. In JavaScript, this is so fundamental it's built into the language and the DOM.
// You've been using Observer every time you did this
button.addEventListener('click', handleClick);
window.addEventListener('resize', handleResize);
eventEmitter.on('data', processData);
Every addEventListener call is registering an observer. The DOM element is the subject. Your callback function is the observer. When the event fires, all registered observers get notified. You never think of it as a "pattern" because it's just how JavaScript works.
Building your own Observer is useful when you need event-like behavior outside the DOM:
type Listener<T> = (data: T) => void;
class EventBus<Events extends Record<string, unknown>> {
private listeners = new Map<keyof Events, Set<Listener<any>>>();
on<K extends keyof Events>(event: K, listener: Listener<Events[K]>): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener);
// Return unsubscribe function
return () => {
this.listeners.get(event)?.delete(listener);
};
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners.get(event)?.forEach(listener => listener(data));
}
}
// Usage with type safety
interface AppEvents {
'user:login': { userId: string; role: string };
'cart:update': { itemCount: number; total: number };
'notification': { message: string; level: 'info' | 'error' };
}
const bus = new EventBus<AppEvents>();
const unsubscribe = bus.on('user:login', ({ userId, role }) => {
console.log(`User ${userId} logged in as ${role}`);
});
bus.emit('user:login', { userId: '123', role: 'admin' });
// Clean up when done
unsubscribe();
The typed EventBus catches mistakes at compile time. Emit an event with the wrong data shape? TypeScript error. Subscribe to an event that doesn't exist? TypeScript error. The unsubscribe function returned by on() prevents the memory leak that comes from forgetting to remove listeners — a pattern React developers know well from useEffect cleanup.
React state management libraries are Observer all the way down. Zustand, Jotai, Redux — they all maintain state and notify subscribed components when it changes. I think understanding Observer helps you understand why useSelector re-renders your component: the store is the subject, your component is the observer, and the selector determines which state changes trigger notification.
Factory Pattern — Creating Objects Without new
The Factory pattern creates objects without exposing the creation logic. In JavaScript, this often means a function that returns different objects based on parameters.
The classic example: a notification system that creates different notification types.
interface Notification {
type: string;
render(): string;
send(): Promise<void>;
}
function createNotification(
type: 'email' | 'sms' | 'push',
recipient: string,
message: string
): Notification {
switch (type) {
case 'email':
return {
type: 'email',
render: () => `<div class="email">${message}</div>`,
send: async () => {
await emailService.send(recipient, message);
}
};
case 'sms':
return {
type: 'sms',
render: () => message.slice(0, 160),
send: async () => {
await smsGateway.send(recipient, message.slice(0, 160));
}
};
case 'push':
return {
type: 'push',
render: () => JSON.stringify({ title: 'New message', body: message }),
send: async () => {
await pushService.send(recipient, { title: 'New message', body: message });
}
};
}
}
// Calling code doesn't care about creation details
const notification = createNotification('email', 'user@example.com', 'Your order shipped');
await notification.send();
The calling code works with the Notification interface. It doesn't need to know that email notifications wrap HTML, SMS truncates to 160 characters, or push notifications require a title. Adding a new notification type (Slack, Discord, webhook) means adding a case to the factory without changing any calling code.
In JavaScript specifically, Factory functions have an advantage over classes: they can return different types with different internal structures while maintaining the same interface. No inheritance hierarchy needed. No new keyword. Just functions returning objects.
Where Factories Appear in Real Code
React component composition is factory-like. A higher-order component is a function that takes a component and returns a new component — a factory for React components.
function withAuth<P extends object>(
WrappedComponent: React.ComponentType<P>,
requiredRole: string
): React.FC<P> {
return function AuthenticatedComponent(props: P) {
const { user } = useAuth();
if (!user) return <LoginRedirect />;
if (user.role !== requiredRole) return <AccessDenied />;
return <WrappedComponent {...props} />;
};
}
const AdminDashboard = withAuth(Dashboard, 'admin');
const EditorPanel = withAuth(Editor, 'editor');
withAuth is a factory. It produces new components with authentication behavior baked in. The wrapped component doesn't know or care about auth — that's the factory's job.
Database connection creation is another natural factory use case:
function createDatabaseConnection(config: DbConfig) {
switch (config.type) {
case 'postgres':
return new Pool({ connectionString: config.url, max: config.poolSize });
case 'sqlite':
return new Database(config.path);
case 'mysql':
return mysql.createPool({ uri: config.url, connectionLimit: config.poolSize });
}
}
One function, consistent interface, different underlying implementations. The rest of the application works with the connection without knowing which database engine is behind it.
Strategy Pattern — Swappable Algorithms
Strategy lets you define a family of algorithms, put each in its own function or object, and make them interchangeable. The classic textbook example is sorting algorithms. The practical JavaScript example is, I think, much more interesting.
// Define strategy types
type PricingStrategy = (basePrice: number, quantity: number) => number;
const standardPricing: PricingStrategy = (price, qty) => {
return price * qty;
};
const bulkPricing: PricingStrategy = (price, qty) => {
if (qty >= 100) return price * qty * 0.7;
if (qty >= 50) return price * qty * 0.8;
if (qty >= 10) return price * qty * 0.9;
return price * qty;
};
const subscriptionPricing: PricingStrategy = (price, qty) => {
return price * qty * 0.75; // 25% subscriber discount
};
const flashSalePricing: PricingStrategy = (price, qty) => {
return price * qty * 0.5; // 50% off
};
// Context that uses the strategy
class ShoppingCart {
private items: CartItem[] = [];
private pricingStrategy: PricingStrategy = standardPricing;
setPricingStrategy(strategy: PricingStrategy): void {
this.pricingStrategy = strategy;
}
getTotal(): number {
return this.items.reduce((total, item) => {
return total + this.pricingStrategy(item.price, item.quantity);
}, 0);
}
}
// Switch strategies at runtime
const cart = new ShoppingCart();
cart.setPricingStrategy(bulkPricing); // For wholesale customers
cart.setPricingStrategy(flashSalePricing); // During flash sales
In JavaScript, Strategy is just "pass a function." You don't need a class hierarchy of strategies or a formal interface. Functions are strategies. The Array.sort() method takes a comparison function — that's Strategy pattern. Express middleware that takes a configuration function — Strategy. Any callback that changes behavior — Strategy.
Validation Strategies
Here's a more realistic example. Form validation where different forms need different validation rules:
type ValidationRule = (value: string) => string | null;
const required: ValidationRule = (value) =>
value.trim() ? null : 'This field is required';
const minLength = (min: number): ValidationRule => (value) =>
value.length >= min ? null : `Must be at least ${min} characters`;
const email: ValidationRule = (value) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : 'Invalid email address';
const matches = (pattern: RegExp, message: string): ValidationRule => (value) =>
pattern.test(value) ? null : message;
// Compose strategies
function validate(value: string, rules: ValidationRule[]): string[] {
return rules
.map(rule => rule(value))
.filter((error): error is string => error !== null);
}
// Usage
const passwordErrors = validate(userInput, [
required,
minLength(8),
matches(/[A-Z]/, 'Must contain an uppercase letter'),
matches(/[0-9]/, 'Must contain a number'),
]);
Each validation rule is a strategy. The validate function doesn't know or care what rules it's running — it applies each one and collects errors. Adding new validation rules requires zero changes to the validation framework. Just write a new function.
This is pattern usage without pattern ceremony. No ValidationStrategy abstract class. No ConcreteStrategyA and ConcreteStrategyB. Just functions.
Module Pattern — JavaScript's Native Encapsulation
Before ES modules, JavaScript had no built-in way to create private state. The Module pattern uses closures to encapsulate private variables and expose a public API.
function createRateLimiter(maxRequests: number, windowMs: number) {
// Private state — not accessible from outside
const requests = new Map<string, number[]>();
function cleanExpired(key: string): void {
const now = Date.now();
const timestamps = requests.get(key) || [];
requests.set(key, timestamps.filter(t => now - t < windowMs));
}
// Public API
return {
isAllowed(key: string): boolean {
cleanExpired(key);
const timestamps = requests.get(key) || [];
if (timestamps.length >= maxRequests) return false;
timestamps.push(Date.now());
requests.set(key, timestamps);
return true;
},
remaining(key: string): number {
cleanExpired(key);
const timestamps = requests.get(key) || [];
return Math.max(0, maxRequests - timestamps.length);
},
reset(key: string): void {
requests.delete(key);
}
};
}
const limiter = createRateLimiter(100, 60_000); // 100 requests per minute
if (!limiter.isAllowed(clientIp)) {
res.status(429).json({ error: 'Too many requests' });
}
The requests Map and cleanExpired function are completely inaccessible from outside. The returned object is the only way to interact with the rate limiter. No way to accidentally corrupt the internal state by directly mutating the Map.
ES modules now provide file-level encapsulation (unexported symbols are private), but the Module pattern is still useful for creating multiple instances with private state — something ES module scope can't do because module scope is singleton.
Middleware Pattern — Chain of Responsibility in Disguise
Express middleware is the Chain of Responsibility pattern adapted for JavaScript's async, function-first nature. Each middleware function receives the request, can process it or modify it, and decides whether to pass it to the next handler.
type Middleware = (
req: Request,
res: Response,
next: () => Promise<void>
) => Promise<void>;
function compose(middlewares: Middleware[]): Middleware {
return async (req, res, next) => {
let index = -1;
async function dispatch(i: number): Promise<void> {
if (i <= index) throw new Error('next() called multiple times');
index = i;
const fn = i < middlewares.length ? middlewares[i] : next;
if (!fn) return;
await fn(req, res, () => dispatch(i + 1));
}
await dispatch(0);
};
}
// Practical middleware
const logging: Middleware = async (req, res, next) => {
const start = Date.now();
await next();
console.log(`${req.method} ${req.url} - ${Date.now() - start}ms`);
};
const auth: Middleware = async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
res.status(401).json({ error: 'Unauthorized' });
return; // Don't call next — chain stops here
}
req.user = verifyToken(token);
await next();
};
const handler = compose([logging, auth, processRequest]);
The beauty of this pattern in JavaScript: each middleware is a simple function. No class hierarchy. No formal interface implementation. Compose them in any order. Add or remove middleware without changing other middleware. The next() call is the decision point — call it to pass to the next handler, don't call it to stop the chain.
This pattern appears everywhere beyond Express. React's useReducer middleware, Redux middleware, Koa's entire architecture, fetch interceptors in libraries like Axios and ky — all variations of the same idea.
Singleton — The Pattern JavaScript Makes Easy (and Dangerous)
Singleton ensures a class has only one instance. In JavaScript, ES modules are naturally singletons — a module is evaluated once and cached. Subsequent imports get the same instance.
// database.ts
import Database from 'better-sqlite3';
const db = new Database('app.db');
db.pragma('journal_mode = WAL');
export { db };
Every file that imports db from this module gets the same database connection. Module caching guarantees it. No special Singleton class needed. This is why most JavaScript singletons are just module-level variables.
Where Singleton gets dangerous in JavaScript: shared mutable state. If your singleton holds mutable state and multiple parts of your application modify it, you get the same class of bugs as global variables — mysterious state changes, order-dependent behavior, tests that pass individually but fail when run together.
// Dangerous: mutable singleton state
const appState = {
currentUser: null as User | null,
theme: 'light',
language: 'en',
};
export { appState };
// Anywhere in the app
appState.theme = 'dark'; // Who changed this? When? Why?
If you need shared mutable state (and sometimes you genuinely do), wrap it with controlled access — the Module pattern from earlier, or a state management library that makes mutations traceable.
Proxy Pattern — Intercepting Operations
The Proxy pattern provides a surrogate for another object to control access to it. JavaScript has a literal Proxy object that implements this at the language level.
function createValidatedObject<T extends object>(
target: T,
schema: Record<keyof T, (value: unknown) => boolean>
): T {
return new Proxy(target, {
set(obj, prop, value) {
const validator = schema[prop as keyof T];
if (validator && !validator(value)) {
throw new TypeError(
`Invalid value for ${String(prop)}: ${JSON.stringify(value)}`
);
}
return Reflect.set(obj, prop, value);
}
});
}
const user = createValidatedObject(
{ name: '', age: 0, email: '' },
{
name: (v) => typeof v === 'string' && (v as string).length > 0,
age: (v) => typeof v === 'number' && (v as number) >= 0 && (v as number) <= 150,
email: (v) => typeof v === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v as string),
}
);
user.name = 'Anurag'; // Works
user.age = 28; // Works
user.age = -5; // Throws TypeError
user.email = 'not-email'; // Throws TypeError
The proxy intercepts property assignments and validates before allowing them. The calling code works with what looks like a plain object. Vue 3's reactivity system uses Proxy internally — when you access a reactive property, the proxy records the dependency; when you set a property, the proxy triggers re-renders. Understanding Proxy helps you understand why Vue's reactivity "just works" on plain objects.
Patterns I Stopped Using
Not every pattern from the textbooks translates well to JavaScript.
Abstract Factory — In Java, this manages families of related objects through class hierarchies. In JavaScript, a regular factory function with a configuration parameter does the same thing with less ceremony. I've never written an Abstract Factory in JavaScript that wasn't overengineered, though I could be wrong about that for larger codebases.
Template Method — Defines an algorithm skeleton in a base class with overridable steps in subclasses. In JavaScript, just pass the varying steps as functions. Strategy makes Template Method redundant when you have first-class functions.
Iterator — JavaScript has this built into the language with for...of, Symbol.iterator, generators, and the iteration protocol. You never need to implement Iterator as a design pattern because it's a language feature.
The patterns that survive in JavaScript are the ones that solve problems functions alone can't: managing subscriptions (Observer), controlling object creation complexity (Factory), encapsulating state (Module), composing behavior chains (Middleware). The structural patterns built around class hierarchies mostly dissolve into simpler functional alternatives.
That's the thing about design patterns in JavaScript — the language's flexibility means many "patterns" are just idiomatic code. You don't need to know the pattern name to use it effectively. But knowing the names helps you communicate with other developers about code structure. Saying "this is a factory" or "we're using strategy here" conveys intent faster than explaining the mechanism. The vocabulary is the value, not the ceremony — at least from what I've seen.
Keep Reading
- Clean Code Without the Dogma — What Actually Matters in Practice — Patterns give you structure, but knowing when to keep things simple matters just as much.
- Next.js App Router Deep Dive — Server Components, Streaming, and the Caching Trap — See how patterns like Factory, Observer, and Middleware show up in a real framework architecture.
Written by
Anurag Sinha
Full-stack developer specializing in React, Next.js, cloud infrastructure, and AI. Writing about web development, DevOps, and the tools I actually use in production.
Stay Updated
New articles and tutorials sent to your inbox. No spam, no fluff, unsubscribe whenever.
I send one email per week, max. Usually less.
Comments
Loading comments...
Related Articles

TypeScript Patterns That Come Up More Than You'd Think
Discriminated unions, template literal types, conditional type extraction, and the satisfies operator. Production patterns, not interview trivia.

SQLite — The Most Underrated Database in Your Toolbox
Why I stopped reaching for Postgres by default and started shipping production apps with SQLite. WAL mode, embedded analytics, and when it genuinely beats the big databases.

Regex Mastery — Stop Copy-Pasting Patterns You Don't Understand
Learning regex properly changed how I handle text processing. Named groups, lookaheads, and real-world patterns I actually use in production.