Here’s what engineering leaders need to know before getting into the patterns themselves.
Key Points
- Patterns matter less for code reuse than for cutting the cost of reading a codebase someone else wrote. They give teams a shared vocabulary that pays off in reviews, maintenance, and onboarding.
- The catalog predates JavaScript by a year. The Gang of Four cataloged 23 patterns in October 1994; JavaScript arrived in December 1995 and has since reshaped several of them.
- Eight patterns and idioms are particularly useful in modern JavaScript. Six trace back to the Gang of Four: Singleton, Prototype, Adapter, Decorator, Observer, and Strategy. Module and a simple Factory idiom round out the list.
- TypeScript doesn’t change which patterns you choose, but it can make their contracts explicit. Adapter and Strategy benefit particularly from compile-time checking. Timing when you introduce a pattern matters more than the pattern itself.
Design patterns are often filed under coding techniques and reduced to a shorthand for code reusability. Their real value sits somewhere else: in how much they cut the cost of reading a codebase someone else wrote.
Patterns give teams a common language. Saying “this wants a Strategy” settles in four words what might otherwise take ten minutes at a whiteboard. That compression of time is the business case.
The catalog is older than JavaScript itself. The Gang of Four cataloged 23 patterns in October 1994, with examples in C++ and Smalltalk. JavaScript, first released the following year in December 1995, progressively reshaped several of them, and the language has since absorbed some of their machinery.
What Are JavaScript Design Patterns?
JavaScript design patterns are reusable solutions to common problems in software design. They have names so teams can use them as technical shorthand. One developer says, “we’ll do a request Adapter,” and the rest of the team pictures the same shape without anyone reading the implementation.
That’s also why there’s a catalog everyone refers to. It came from the book Design Patterns: Elements of Reusable Object-Oriented Software, which Addison-Wesley first published in October 1994. The authors were Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, known collectively as the Gang of Four, or GoF.
They cataloged 23 patterns, with examples in C++ and Smalltalk. The first version of JavaScript arrived the year after the book was released.
Why should a VP of Engineering care about a C++ book from 1994? Because engineers spend substantial time trying to understand existing code, not just writing new code. A field study of 78 professional developers across seven industrial projects, published in IEEE Transactions on Software Engineering, logged 3,148 working hours and found that roughly 58% of that time fell into a category the researchers called “program comprehension.”
Treat that percentage as evidence of the scale of the problem, not as a figure to generalize from. The study’s companies used Java and .NET primarily, and it measured how comprehension time is spent, not whether design patterns reduce it.
A more recent read on the same pressure: Sonar’s 2026 State of Code survey found that developers spend an average of 24% of their work week on toil. The report says that, for less frequent AI users, toil still includes debugging poorly documented code and understanding existing systems.
Comprehension hasn’t gone away; its sources have shifted.
While the numbers don’t offer conclusive proof that patterns solve the problem, they do explain why the cost is worth attacking. A reviewer who recognizes an Adapter can stop reverse-engineering the wrapper’s overall role and focus on whether the translation is correct.
Onboarding new developers takes less time for the same reason. None of that requires the pattern itself to be particularly clever. It just requires someone else to recognize what you’re doing.
How JavaScript and TypeScript Change the Classic Patterns
The 1994 catalog is grounded in class-based object-oriented design, with examples in statically typed C++ and dynamically typed Smalltalk.
JavaScript changes the inheritance model: object-oriented programming is built on prototypes, even when class syntax is used. TypeScript adds optional static checking. Both facts change how these eight patterns and idioms look in practice, which is why this discussion comes before the individual patterns.
Classes, Prototypes, and Delegation

Class syntax and the prototype chain it creates.
A prototype is an object that another object delegates to. A class is syntax for wiring up that delegation. Two differences between the two styles shape the patterns below.
Behavior resolves by delegation. One object holds a reference to another, and a failed lookup keeps walking that reference until the property turns up. The chain ends at an object whose prototype is null; calling Object.getPrototypeOf() on that object returns null.
MDN covers the mechanics in its guide to inheritance and the prototype chain. Nothing gets copied anywhere in that process.
So you’ll read that classes are pure syntactic sugar over delegation. That’s close enough for most purposes, and MDN describes them as mainly an abstraction over prototypal inheritance.
The first difference is a limit in JavaScript itself. JavaScript has no private-constructor syntax, so runtime enforcement usually means hiding the constructor behind a module or closure, or adding a guard. TypeScript’s private constructor is checked at compile time and doesn’t exist in the emitted JavaScript.
The second difference runs the other way, and it’s pure JavaScript. Class syntax gives you #private fields, which the older constructor-function style can’t express, and the engine enforces them at runtime rather than any tooling. Closures and WeakMap can provide comparable privacy without a class, which is why the Factory example below uses a closure and never needs #private.
Some bugs involve behavior the object never declared. Finding which ancestor supplied it means walking the prototype chain by hand.
Those chains get long in a large codebase, and reading them quickly is a skill senior JavaScript engineers bring from other projects rather than learn on yours.
What TypeScript Adds With Static Types
Some numbers on TypeScript versus JavaScript, current as of 2025 and 2026: GitHub’s Octoverse 2025 report counted 2,636,006 monthly TypeScript contributors in August 2025, making TypeScript the most-used language on GitHub by that measure and putting it roughly 42,000 contributors ahead of Python.
Stack Overflow’s 2025 developer survey still puts JavaScript first for self-reported usage, at 66% of respondents who answered its language question. Two measures, two different answers, both true at the same time: commit activity and self-reported usage aren’t the same question.
One of these dates matters for the Decorator section further down. March 2023 is when TypeScript 5.0 picked up support for the newer ECMAScript decorators proposal without requiring experimentalDecorators. That split still matters when you’re evaluating framework compatibility.
The second is context rather than constraint. TypeScript 7.0 reached general availability on July 8, 2026, as a native Go port of the compiler. Microsoft describes it as a 10x faster native port, with performance varying by workload. It changes build performance, not which patterns you can write.
TypeScript turns many of the contracts a pattern implies into contracts the compiler can verify.
| Concern | JavaScript | TypeScript | Which Patterns Benefit |
| Encapsulation | closures and #private fields, enforced at runtime | private, protected, readonly, enforced by the type checker | Module and some Singleton implementations, which encapsulate shared state |
| Contracts | Structural expectations validated by tests or explicit validation | Interface declarations checked at compile time, absent at runtime | Adapter and Strategy, which are both promises about shape |
| Initialization | Manual assignment in the constructor | Parameter properties in the constructor signature | Factory and Adapter, which both assemble an object from injected parts |
| Payload shape | Whatever was published | Generic event-type definitions | Observer, where a wrong payload otherwise surfaces only at runtime |
The Three Categories of Design Patterns: Creational, Structural, Behavioral
The Gang of Four catalog groups design patterns by the problems they solve, so each category addresses a different type of question.
A few examples:
- How is this object created? That’s Creational.
- How do the parts combine into something larger without welding themselves together? That’s Structural.
- After the parts exist, how do they communicate with one another? That’s Behavioral.
Below is the table to scan if you’d rather skip the code examples.
| Pattern | Category | What It Solves | When to Use It |
| Singleton | Creational | One instance, one shared state | Genuinely shared resources such as connection pools or feature-flag clients |
| Factory * | Creational | Decouples creation from the caller | Several entity types, or setup logic the caller shouldn’t own |
| Prototype | Creational | Creates objects by copying a configured exemplar | Repeatedly creating similarly configured objects. JavaScript prototype delegation is a separate mechanism with the same name |
| Module * | Structural | Private scope with an explicit public API | The default in modern JavaScript. ES modules provide it natively |
| Adapter | Structural | Bridges incompatible interfaces | Third-party SDKs, legacy APIs, or anything you may replace |
| Decorator | Structural | Adds behavior without modifying the original | Cross-cutting concerns such as logging, authorization, retries, and telemetry |
| Observer | Behavioral | Notifies subscribers when state changes | Event-driven flows, reactive UI, and real-time feeds |
| Strategy | Behavioral | Makes algorithms interchangeable | Replacing branching that encodes business rules |
* Neither is in the GoF catalog. That catalog has Factory Method and Abstract Factory rather than a plain Factory, and no Module pattern at all. Both are here because of how often they turn up in JavaScript.
Creational Patterns Control How Objects Get Made
Every call site that builds its own objects can duplicate setup logic. Objects then arrive in whatever state that particular call site happened to leave them. The three patterns below offer reusable approaches to object construction, each changing how that construction is controlled.
Singleton Pattern
A Singleton provides one shared instance within a defined runtime scope. Only one instance exists there, and every caller inside that scope receives the same object with the same state.
You typically don’t need to write one yourself. Inside a single module graph, every import that resolves to the same module identity shares one evaluated instance, so anything declared at the top level of an ES module is effectively a singleton within that module graph.
1// db.js
2
3const pool = new Map();
4
5export function getConnection(id) {
6 return pool.get(id);
7}
8
9export function addConnection(id, conn) {
10 pool.set(id, conn);
11}There’s no class here, no defensive constructor, and no getInstance(). That last one is a naming convention carried over from Java and the Gang of Four book, not a JavaScript or TypeScript API. Each file that imports db.js uses the same Map.
But there are two possible pitfalls. A worker, another process, or a duplicated package copy in node_modules each gets its own instance. “The one” instance applies only inside a module graph, not across your entire deployment.
The other issue is the one every senior engineer has discovered at least once. A mutable Singleton is a form of global state dressed in a more attractive outfit.
Shared mutable Singleton state can make tests order-dependent unless it’s reset or isolated between tests. Mutable request-specific state stored at module scope can leak from one request into the next under server-side rendering. Reach for a Singleton only when the resource involved is genuinely shared, and prefer injecting it over importing it deep within business logic.

Four importers, one evaluated module instance.
Factory Pattern
A Factory moves construction and configuration behind a function, so the caller doesn’t own the setup procedure or depend directly on implementation details.
In JavaScript, this is commonly implemented with a closure that returns an object literal:
1const RANKS = { debug: 0, info: 1, warn: 2, error: 3 };
2
3export function createLogger({ level = "info", prefix = "" } = {}) {
4 // Object.hasOwn, not "level in RANKS":
5 // the in operator also matches inherited keys such as "toString"
6 if (!Object.hasOwn(RANKS, level)) {
7 throw new Error(`Unknown log level: ${level}`);
8 }
9
10 const threshold = RANKS[level];
11
12 const write = (rank, msg) => {
13 if (RANKS[rank] >= threshold) console[rank](prefix + msg);
14 };
15
16 return {
17 debug: (msg) => write("debug", msg),
18 info: (msg) => write("info", msg),
19 warn: (msg) => write("warn", msg),
20 error: (msg) => write("error", msg),
21 };
22}
23
24const apiLog = createLogger({ level: "warn", prefix: "[api] " });
25apiLog.warn("rate limit at 80%");The returned methods escape, but the closed-over bindings don’t. Callers have no direct access to threshold or write; they can interact with the closed-over state only through the returned API. Like fields marked with #private, closures provide similar runtime privacy without requiring the definition of classes.
From an organizational standpoint, a Factory lets you change construction rules without updating every call site that creates the object.

One factory function can create multiple configured loggers.
Prototype Pattern
The 1994 GoF book, together with JavaScript, shares the word “prototype.” The GoF used it to mean cloning: keep a template around and stamp copies off it. In JavaScript, a prototype is a delegation link, a different mechanism that happens to carry the same name.
JavaScript offers two tools that are often confused here, and choosing the wrong one creates real bugs.
1// Delegation: myCar links to baseVehicle. Nothing is copied.
2const baseVehicle = { wheels: 4, start() { return "running"; } };
3const myCar = Object.create(baseVehicle);
4myCar.make = "Toyota";
5console.log(myCar.wheels); // 4, resolved through the chain
6
7// Cloning: an independent deep copy of plain data
8const baseConfig = { retries: 3, headers: { agent: "core" } };
9const testConfig = structuredClone(baseConfig);
10testConfig.headers.agent = "test";
11console.log(baseConfig.headers.agent); // still "core"Use Object.create() to establish delegation links for a new object and use structuredClone() to perform deep copies of supported data structures and handle circular references. Note that attempting to clone a function throws DataCloneError. Property descriptors, getters and setters, custom prototype chains, and class private elements aren’t preserved during cloning operations either.
structuredClone() is primarily a data-cloning facility, not a general-purpose object-cloning protocol. A custom class instance shouldn’t be expected to retain its original prototype behavior.

1Object.create() delegates; structuredClone() copies.Structural Patterns Compose Objects Without Coupling Them
Structural patterns determine how objects and interfaces are composed. They decide whether real-world scenarios like swapping your SMS provider, payment processor, or search backend touch one file or three hundred.
Module Pattern
The Module pattern encapsulates private state behind a publicly declared API. Before ES modules, JavaScript commonly emulated this pattern with immediately invoked functions and other module conventions. The native import and export system made the module boundary part of the language.
1// inventory.js
2
3const cache = new Map(); // private: never exported, not importable outside
4
5export function getSku(sku) {
6 return cache.get(sku);
7}
8
9export async function refresh() {
10 const res = await fetch("/api/inventory");
11 for (const item of await res.json()) cache.set(item.sku, item);
12}TypeScript includes something that has no analog in JavaScript. Types brought in with import type are erased when the code compiles, so a shared type definition adds zero bytes to the production bundle.
1// inventory.ts
2
3export type Sku = { sku: string; onHand: number };
4
5const cache = new Map<string, Sku>();
6
7export function getSku(sku: string): Sku | undefined {
8 return cache.get(sku);
9}
10
11// consumer.ts
12
13import type { Sku } from "./inventory"; // erased at compile time
14import { getSku } from "./inventory"; // emitted

Only exported bindings cross the module boundary.
Adapter Pattern
An Adapter wraps something with an incompatible interface and exposes the interface your application expects. This is the pattern with the clearest cost argument in the catalog.
A good example is an SMS vendor whose client you can’t change.
1// Third-party client we do not control
2class VendorSms {
3 send(to, body) { /* vendor-specific call */ }
4}
5
6// The interface our application actually calls
7class SmsGateway {
8 constructor(client) { this.client = client; }
9
10 notify(recipient, message) {
11 return this.client.send(recipient.phone, message);
12 }
13}
14
15const gateway = new SmsGateway(new VendorSms());Each call site depends on SmsGateway. If the vendor deprecates send(), or purchasing finds a better deal, the vendor-specific translation stays concentrated in the adapter.
TypeScript makes this arrangement into something the compiler can enforce. Declare the interface, and a new adapter that doesn’t meet that interface fails during type checking before running.
1declare class VendorSms {
2 send(to: string, body: string): Promise<void>;
3}
4
5interface Notifier {
6 notify(recipient: { phone: string }, message: string): Promise<void>;
7}
8
9class SmsGateway implements Notifier {
10 constructor(private client: VendorSms) {}
11
12 async notify(recipient: { phone: string }, message: string) {
13 await this.client.send(recipient.phone, message);
14 }
15}Note private client in the constructor signature. That is a parameter property: it declares and initializes the field, with a visibility modifier, in one line.

The adapter absorbs the vendor’s interface.
Decorator Pattern
A Decorator wraps an object or function to add functionality without modifying it. It’s how logging and retry attempts get added to functions without cluttering up their code.
It’s a flexible alternative to subclassing when you want to extend individual objects without growing an inheritance tree. React higher-order components apply the same wrapping idea at the component level.
Plain JavaScript requires no additional syntax for this. A higher-order function is the functional version and behaves similarly when composing.
1function withRetry(fn, attempts = 3) {
2 return async function (...args) {
3 for (let i = 1; i <= attempts; i++) {
4 try {
5 return await fn.apply(this, args);
6 } catch (err) {
7 if (i === attempts) throw err;
8 }
9 }
10 };
11}
12
13const chargeCard = withRetry(rawChargeCard);
14// stacked: withLogging(withRetry(rawChargeCard)) logs the outermost callTypeScript 5.0 added support for the newer ECMAScript decorators proposal using the familiar declarative @ syntax. TypeScript had supported the same syntax for years under its older legacy decorator model.
Enabling experimentalDecorators selects the legacy semantics instead. Despite the shared terminology, this syntax for transforming classes and class elements isn’t itself the GoF Decorator pattern. TypeScript’s newer decorators are type-checked and emitted differently from the legacy implementation, and the newer model isn’t compatible with emitDecoratorMetadata or parameter decorators.
That’s the migration trap. Legacy method decorators receive a target, property key, and descriptor; the newer model receives the method and a context object. If a framework depends on constructor metadata or parameter decorators, switching semantics can break that framework outright.
1function timed(originalMethod: any, context: ClassMethodDecoratorContext) {
2 return async function (this: any, ...args: any[]) {
3 const label = String(context.name);
4 console.time(label);
5 try {
6 return await originalMethod.apply(this, args);
7 } finally {
8 console.timeEnd(label); // after the promise settles, not before
9 }
10 };
11}
12
13class Reports {
14 @timed
15 async build(rows: Row[]) { /* heavy work */ }
16}Many existing Angular, NestJS, and TypeORM projects still depend on the legacy decorator model and, in some cases, emitted decorator metadata. Their underlying mechanisms differ, so migration has to track each framework’s compatibility individually.
A single compilation can’t have legacy semantics for some decorators while using newer semantics for others. Microsoft has said the legacy experimentalDecorators flag will remain for the foreseeable future.
The takeaway for an engineering leader is short. Migrate decorators to match what your frameworks support, rather than treating them as a compiler-flag cleanup. Validate support for your specific framework versions before you schedule the work.

Wrapping layers add behavior. The core stays unchanged.
Behavioral Patterns Manage Communication and Change
Behavioral patterns make communication between multiple objects less coupled. These are the patterns you encounter when building any non-trivial frontend.
Observer Pattern
The Observer pattern lets a subject notify an arbitrary number of subscribers when its state changes, without knowing who they are or how many exist.
Many event-handling systems run on the same idea. A DOM listener and a Node EventEmitter share the same basic shape underneath.
1export function createStore(initial) {
2 let state = initial;
3 const listeners = new Set();
4
5 return {
6 // stable reference: replace state on write, never mutate it in place
7 getSnapshot: () => state,
8
9 subscribe(fn) {
10 listeners.add(fn);
11 return () => listeners.delete(fn); // the leak fix
12 },
13
14 set(next) {
15 state = next;
16 for (const fn of listeners) fn();
17 },
18 };
19}The returned cleanup function addresses the pattern’s most immediate lifecycle risk. Subscribers that never get removed keep receiving notifications after their components unmount, and they can keep their listener closures, along with anything those closures capture, in memory. That’s the lapsed listener leak.
React provides an API for this exact pattern. useSyncExternalStore requires a subscribe function to return its own unsubscribe, along with a getSnapshot function that must return a cached value, not a newly created object each time it’s called.
It also prevents components from observing inconsistent external-store snapshots during concurrent rendering; server rendering needs a getServerSnapshot function alongside it. For teams using React at scale, a subscription without a corresponding cleanup should be a blocker in code review.

The subject notifies; the subscriber owns cleanup.
Strategy Pattern
Strategy provides the ability to replace one algorithm with another at runtime. It’s useful once branching has quietly become the place your interchangeable business rules live. Those strategies can be plain functions or objects, not only classes, and the usual trigger is a switch statement that has grown over time.
1const shipping = {
2 standard: (kg) => 5 + kg * 0.4,
3 express: (kg) => 12 + kg * 0.9,
4 freight: (kg) => 60 + kg * 0.15,
5};
6
7function quote(method, weightKg) {
8 // Object.hasOwn, not a truthy check: shipping["toString"] returns an inherited function
9 if (!Object.hasOwn(shipping, method)) {
10 throw new Error(`Unknown shipping method: ${method}`);
11 }
12 return shipping[method](weightKg);
13}Adding a fourth method means adding a key. The existing pricing strategies stay untouched, which can shrink the regression surface.
Untyped, though, a strategy is only a convention. TypeScript turns it into a contract.
1interface ShippingRate {
2 label: string;
3 price(weightKg: number): number;
4}
5
6const express: ShippingRate = {
7 label: "Express",
8 price: (kg) => 12 + kg * 0.9,
9};
10
11function quote(rate: ShippingRate, weightKg: number) {
12 return rate.price(weightKg);
13}Now a new strategy that leaves out price() fails at build time rather than at checkout.

One signature, interchangeable implementations.
Patterns in Modern JavaScript, and When Not to Force Them
Several JavaScript patterns have been absorbed or simplified, some by the platform and some by modern frameworks. ES modules made the Module pattern native and usually removed the need for an explicit Singleton class, though they didn’t remove the risks of shared mutable state.
structuredClone() addresses the data copying portion of Prototype, while useSyncExternalStore gave the Observer pattern a supported home in React.
That absorption is neither new nor specific to JavaScript. Peter Norvig made the argument in 1996, finding that 16 of the 23 Gang of Four patterns became invisible or simpler in Lisp because first-class functions and modules already did the work. JavaScript has both.
Paul Graham put the same observation less diplomatically in Revenge of the Nerds: “When I see patterns in my programs, I consider it a sign of trouble.”
His point is that repeated structure can mean the abstraction underneath is too weak, and the programmer is hand-expanding a macro they should have written. He exaggerates, but a JavaScript team creating an AbstractHandlerFactoryProvider is generally supporting his position.
Should You Avoid a Design Pattern?
There’s one question you can ask in a design review without sounding like a roadblock.
Build the extension point once someone names the requirement it serves. Nobody can name one? Then it’s a hook that will never get used, and the cost lands on whoever reads the file next.
Sandi Metz describes where that ends up in The Wrong Abstraction. An engineer creates a common abstraction, and a potential requirement appears close enough to fit; the next engineer adds parameters. Then another. No one feels entitled to intervene, so the conditional statements keep accumulating until the file becomes increasingly hard to change. She concludes that duplication costs less than using the wrong abstraction.
Dan Abramov arrived at the same conclusion from within the React ecosystem. In Goodbye, Clean Code, he rewrote a colleague’s repetitive code into a more elegant abstraction and watched it become the most difficult file to modify in the entire project: “Let clean code guide you. Then let it go.”
The business version of this issue is consistency. Two half-applied patterns in one repository cost more than one adequate pattern applied consistently, since the reader has to work out which convention is in force before reading anything else.
Document the small set of patterns the codebase actually uses, with an example of each and a note on what it’s for. Then enforce it in reviews. This is the step most organizations miss.
Choosing Patterns Deliberately
There’s a defensible bias worth naming here: many senior engineers reach for Patterns of Enterprise Application Architecture before the Gang of Four when a real design question comes up. The GoF catalog is worth knowing the way any professional vocabulary is worth knowing. You can build serious systems without it, especially when working in a functional style.
At an organizational level, the useful artifact is a one-page document naming the four or five patterns the codebase actually uses, with an example of each and a note explaining its purpose.
On day one, new engineers read about them. Instead of arguing during reviews, reviewers refer to them. What stays off the list are the patterns that don’t belong, and that’s where the organization saves money.


