Every language has gaps. JavaScript has no interfaces at runtime, Java made you write a class for everything, Go has no inheritance at all. Over decades, developers kept running into the same design problems inside those gaps and kept arriving at the same shapes of solution. A design pattern is one of those shapes, written down with a name, so the next person does not have to rediscover it.

Before learning any individual pattern, it helps to answer five questions: what a pattern is, where the catalogue comes from, whether you can skip it, how to study it, and how to do that quickly. This chapter answers them and sets up the rest of the guide.

What a design pattern is

A pattern is a reusable description of a solution to a problem that keeps coming back in a given context. It is not a library and not a snippet you paste. It is closer to a recipe: it names the participants (the classes, functions or modules involved), says what each one is responsible for, and lists the trade-offs.

The value is that you start from designs that have already worked in many systems, instead of inventing your own under deadline pressure.

Where the 23 classic patterns come from

The catalogue most people mean is the "Gang of Four" book, Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, published in 1994. The idea of a pattern language was borrowed from the architect Christopher Alexander, who used it to describe recurring solutions in buildings and towns.

The book lists 23 patterns in three families:

Family Count Question it answers Patterns
Creational 5 How do objects get created? Factory Method, Abstract Factory, Builder, Prototype, Singleton
Structural 7 How are objects composed into bigger structures? Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
Behavioral 11 How do objects talk and share work? Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor

Simple Factory, which you will meet in chapter 5, is not one of the 23. It is a common simplification of Factory Method and a good first step.

None of this is specific to Java or C++. The examples in the book are object-oriented, but the problems (decoupling creation, wrapping incompatible APIs, notifying many listeners) show up in every language and in frameworks you already use. Express middleware is a chain of responsibility, DOM events are observers, and NestJS's dependency injection container hands you singletons by default.

Can you skip them?

You can ship software without ever naming a pattern. You should still learn them, for four reasons:

  1. They are proven. Each pattern is the distilled experience of many teams. Using one usually gets you code that is easier to extend and reuse than a design invented on the spot.
  2. They are vocabulary. Saying "put a facade in front of the payment SDKs" or "make the config a singleton" transfers a whole design in one sentence. Code review and design discussions get much shorter.
  3. They make handover easier. When you get promoted, change teams or change jobs, someone else inherits your code. If the design follows known patterns, they understand the intent without needing to ask you. The reverse is true when you join a new codebase: spotting the patterns is the fastest way to read it.
  4. They teach object-oriented thinking. Patterns are where ideas like "program to an interface", "prefer composition over inheritance" and "encapsulate what varies" stop being slogans and become concrete code.

How to study a pattern

You do not need to grind through the book cover to cover. What you need is a consistent checklist for each pattern. After studying one, you should be able to answer:

  1. Intent. In one sentence, what is this pattern for?
  2. Problem. What real problem does it solve? Find one example from everyday life and one from software.
  3. Applicability. When should you reach for it, and what are the signs that you should not?
  4. Practice. Write it. Reading about a pattern without using it leaves almost nothing behind. Use it at least once in a real or practice project.

Every chapter in this guide follows that checklist, and each one ends with exercises for step 4.

One warning is worth taking seriously from day one: do not try to use every pattern in one system. A toolbox is not a checklist. A carpenter who owns forty tools does not use all forty on one chair, and a codebase that uses a pattern where a function would do is harder to read, not easier.

Rank patterns before you study them

The most practical way to plan your study is a table that rates every pattern on two axes, difficulty and popularity, and then picks the order from it:

  • Easy and popular: study first. Singleton is about one star of difficulty and four stars of popularity.
  • Hard but popular: study next, slowly, with code.
  • Hard and rarely used: skip for now and come back at the end.

Here is my version of that table for TypeScript and Node.js work in 2026, with the chapter that covers each pattern. The stars are a judgement call, not a measurement, so adjust them to the stack you work in.

Pattern Family Difficulty Popularity in JS/TS Chapter
Singleton Creational ★☆☆☆☆ ★★★★☆ 6
Simple Factory / Factory Method Creational ★★☆☆☆ ★★★★★ 5
Builder Creational ★★★☆☆ ★★★☆☆ 8
Prototype Creational ★★☆☆☆ ★★★☆☆ 7
Adapter Structural ★★☆☆☆ ★★★★☆ 9
Facade Structural ★★☆☆☆ ★★★★★ 12
Proxy Structural ★★★☆☆ ★★★★☆ 11
Bridge Structural ★★★☆☆ ★★★☆☆ 10
Strategy Behavioral ★★★☆☆ ★★★★☆ 13
Observer Behavioral ★★★☆☆ ★★★★★ 14
Decorator, Composite, Command, Chain of Responsibility, State, Template Method, Iterator Mixed ★★–★★★ ★★★☆☆ Next round
Abstract Factory, Flyweight, Mediator, Memento, Visitor, Interpreter Mixed ★★★–★★★★ ★★☆☆☆ Leave for last

Ten patterns out of 23 is not a shortcut. It is roughly the set you will meet in a normal week of backend or frontend work, and a good one-week plan: two patterns a day, each with the four-question checklist and one exercise.

A first refactor with a pattern's shape

The most common refactor toward a pattern starts with a switch. Here is a shipping fee function written the obvious way:

type Carrier = "ghn" | "ghtk" | "viettel-post";

export function shippingFee(carrier: Carrier, weightKg: number): number {
  switch (carrier) {
    case "ghn":
      return 15_000 + weightKg * 5_000;
    case "ghtk":
      return 18_000 + weightKg * 4_000;
    case "viettel-post":
      return 20_000 + Math.max(0, weightKg - 2) * 3_500;
  }
}

It works, but every new carrier means editing this function, and the rules for every carrier live in one place that everyone touches. Turning the cases into data gives you a lookup table:

type FeeRule = (weightKg: number) => number;

const feeRules: Record<Carrier, FeeRule> = {
  ghn: (kg) => 15_000 + kg * 5_000,
  ghtk: (kg) => 18_000 + kg * 4_000,
  "viettel-post": (kg) => 20_000 + Math.max(0, kg - 2) * 3_500,
};

export function shippingFee(carrier: Carrier, weightKg: number): number {
  return feeRules[carrier](weightKg);
}

Each rule is now a small function with one job, adding a carrier is adding an entry, and Record<Carrier, FeeRule> makes TypeScript fail the build if a carrier is missing. That is the core of the Strategy pattern (chapter 13) and a direct application of the Open/Closed Principle (chapter 4), written without a single class.

Patterns hiding in a framework you already use

Before writing any pattern yourself, it is worth seeing how many you already depend on. Most of my day-to-day work is in NestJS, and nearly every feature of the framework is a named pattern with a Nest-specific API on top:

What you write in Nest Pattern underneath Chapter
@Injectable() with the default scope Singleton, owned by the container instead of a static field 6
{ provide, useFactory, inject } Simple Factory 5
{ provide: TOKEN, useClass: ... } swapped per environment Strategy plus Dependency Inversion 13
ConfigurableModuleBuilder, forRoot()/forRootAsync() Builder and Factory for whole modules 8
Interceptors (NestInterceptor) Proxy around a route handler 11
@nestjs/platform-express and @nestjs/platform-fastify Adapter: one HttpAdapter interface, two HTTP servers 9
Middleware, guards, pipes, filters run in order Chain of Responsibility Next round
@nestjs/event-emitter with @OnEvent() Observer 14
A module that exports one service over five internal ones Facade 12

One name in that list is a trap. TypeScript decorators (@Injectable(), @Get(), @UseGuards()) are not the Gang of Four Decorator pattern. They attach metadata to a class or method at definition time, and Nest reads that metadata at bootstrap. The GoF Decorator wraps an object at runtime with another object that has the same interface. Interceptors are much closer to the GoF Decorator (or Proxy) than the @ syntax is. Mixing the two up in an interview is a common way to lose a point.

In NestJS

Here is the shipping fee lookup from the previous section, moved into a Nest service the way I would write it at work. The table of rules becomes a provider behind an injection token, so the service depends on the shape of a rule set, not on the numbers:

import { Inject, Injectable, Module } from "@nestjs/common";

export type Carrier = "ghn" | "ghtk" | "viettel-post";
export type FeeRule = (weightKg: number) => number;
export type FeeRules = Record<Carrier, FeeRule>;

export const FEE_RULES = Symbol("FEE_RULES");

export const defaultFeeRules: FeeRules = {
  ghn: (kg) => 15_000 + kg * 5_000,
  ghtk: (kg) => 18_000 + kg * 4_000,
  "viettel-post": (kg) => 20_000 + Math.max(0, kg - 2) * 3_500,
};

@Injectable()
export class ShippingFeeService {
  constructor(@Inject(FEE_RULES) private readonly rules: FeeRules) {}

  quote(carrier: Carrier, weightKg: number): number {
    if (!Number.isFinite(weightKg) || weightKg <= 0) throw new RangeError("Weight must be positive");
    return this.rules[carrier](weightKg);
  }
}

@Module({
  providers: [{ provide: FEE_RULES, useValue: defaultFeeRules }, ShippingFeeService],
  exports: [ShippingFeeService],
})
export class ShippingModule {}

Three patterns are already at work in about thirty lines. ShippingFeeService is a singleton because the default scope creates it once per application. FEE_RULES with useValue is the Strategy table from before, now replaceable without touching the service. And the module is a tiny facade: other modules import ShippingModule and see only ShippingFeeService.

The payoff shows up in tests. Nest's testing module builds the same container your app uses, and lets you replace any provider:

import { Test } from "@nestjs/testing";

it("uses whatever rule set the container provides", async () => {
  const moduleRef = await Test.createTestingModule({ imports: [ShippingModule] })
    .overrideProvider(FEE_RULES)
    .useValue({ ghn: () => 1, ghtk: () => 2, "viettel-post": () => 3 })
    .compile();

  const service = moduleRef.get(ShippingFeeService);
  expect(service.quote("ghtk", 10)).toBe(2);
  expect(() => service.quote("ghn", 0)).toThrow(RangeError);
});

That test does not care about real prices, so it will not break when finance changes a number. A separate, boring test of defaultFeeRules covers the numbers. Splitting "is the wiring right?" from "are the numbers right?" is a habit worth building from chapter 1.

How I use this when reading a new codebase

When I join a Nest codebase, I read three things before any business logic: the AppModule imports, every custom provider (provide: with useFactory, useClass or useValue), and every global enhancer registered with APP_GUARD, APP_INTERCEPTOR, APP_PIPE or APP_FILTER. Custom providers tell you where the team made a decision about creation or substitution. Global enhancers tell you what happens to every request before your code runs. In a 50,000-line service this usually takes under an hour and maps most of the architecture.

A trade-off I would raise in code review

Injection tokens make substitution cheap, and that is exactly why they get overused. If a pull request adds a token, an interface and a factory provider for something with one implementation that is never replaced in tests, I ask the author what second implementation they expect and when. Every token is one more hop when someone uses "go to definition" and lands on @Inject(FEE_RULES) instead of the code. One token per real seam (payment provider, clock, external API, rule set) is a good budget. A token for every service is a project that is harder to navigate than the switch it replaced.

When not to bother

Patterns cost indirection. Skip them when:

  • The code is small, used in one place and unlikely to change. Three if branches are fine.
  • You are guessing at future requirements. Introduce a pattern when the second or third variation actually arrives, not in anticipation.
  • The language or framework already gives you the pattern. A JavaScript module is already a singleton; Array.prototype.map with a callback is already a strategy; Node's EventEmitter is already an observer.

Exercises

  1. Copy the table above and rate each pattern again for the stack you use at work. Pick the five you will study first.
  2. For Singleton, write down one example from everyday life and one from software before reading chapter 6. Check your answer afterwards.
  3. Find a switch statement in a codebase you work on that branches on a type or a status. Rewrite it as a lookup table of functions and see whether the call sites get simpler.
  4. Find one pattern already used by a framework you rely on (Express, NestJS, React, Prisma) and name it.
  5. In a Nest project, list every custom provider and every APP_* global enhancer. For each one, write the pattern it implements in one word, and mark the ones you would delete because they have a single implementation that is never swapped.