A handful of design patterns show up in almost every codebase: Strategy, Factory, Observer, Decorator, Adapter, and Builder, plus Singleton, which is worth knowing mostly so you can recognize its costs. Each pattern is a named, reusable answer to a recurring design problem, and knowing the names lets teams discuss structure quickly. This guide covers the design patterns every developer should know, the problem each one solves, a TypeScript example, and the signals that tell you a pattern is not worth it.

What are design patterns?

A design pattern is a proven structure for solving a common problem in object-oriented or modular code. Patterns are not libraries you install; they are shapes you recognize and adapt. The classic catalog groups them into three families:

  • Creational patterns control how objects are built: Factory, Builder, Singleton.
  • Structural patterns control how objects are composed: Adapter, Decorator, Facade, Proxy.
  • Behavioral patterns control how objects communicate: Strategy, Observer, Command, State.

Most patterns are applications of a few underlying ideas, especially "program to an interface" and "prefer composition over inheritance". If you already know the SOLID principles explained with TypeScript examples, you will see those principles at work in every pattern below.

Design patterns at a glance

Pattern Family Problem it solves Common real-world use
Strategy Behavioral Swap an algorithm at runtime Pricing rules, sorting, retry policies
Factory Creational Hide which concrete class gets created Parsers, payment providers, DB drivers
Observer Behavioral Notify many listeners of a change UI events, domain events, pub/sub
Decorator Structural Add behavior without subclassing Logging, caching, retries around a client
Adapter Structural Make an incompatible interface fit Wrapping third-party SDKs
Builder Creational Construct complex objects step by step HTTP requests, query builders, test data
Singleton Creational Guarantee one shared instance Config, connection pools (with caveats)

Strategy pattern

The Strategy pattern defines a family of interchangeable algorithms behind a common interface and lets the caller choose one at runtime. It replaces branching like if (type === "express") ... else if (type === "standard") ... with polymorphism.

Use it when you have several ways to do the same job and the choice depends on configuration, user input, or context. In languages with first-class functions, a strategy can simply be a function parameter. See the strategy pattern guide for a longer walkthrough.

Factory pattern

A factory centralizes object creation so callers ask for "a payment processor for this region" without knowing which class they get. The simplest form is a function that returns an interface type; the Factory Method pattern moves that decision into subclasses.

Use a factory when construction logic is non-trivial or when the concrete type depends on runtime data. Skip it when there is only one implementation and a constructor call is perfectly clear.

Observer pattern

The Observer pattern lets a subject publish events to any number of subscribers without knowing who they are. This decouples the code that detects a change from the code that reacts to it: an order service emits "order placed", and email, analytics, and inventory each subscribe independently.

Watch for two pitfalls: forgotten subscriptions that leak memory, and cascades where one event triggers many hidden side effects that are hard to trace. Always return an unsubscribe handle. The observer pattern guide covers these trade-offs.

Decorator pattern

A decorator wraps an object that implements an interface and returns another object with the same interface plus extra behavior. Because the wrapper and the wrapped object share a type, decorators stack: logging around caching around retries around a real HTTP client.

Decorators beat inheritance when you need combinations of behaviors. With subclasses you would need a class for every combination; with decorators you compose them at construction time. Read the decorator pattern guide for more composition examples.

Adapter pattern

An adapter converts one interface into another that your code expects. It is the standard way to isolate third-party libraries: your domain defines a Storage interface, and an S3StorageAdapter translates calls to the vendor SDK. If the vendor changes or you migrate providers, only the adapter changes.

Adapter and Decorator look similar in code. The difference is intent: an adapter changes the interface, while a decorator keeps it and adds behavior.

Builder pattern

A builder constructs a complex object step by step, with readable method names instead of a long list of positional constructor arguments. It is especially useful when many fields are optional, when validation should happen once at the end, or when you want immutable results.

In TypeScript, an options object often covers simple cases. Reach for a builder when construction has ordering rules, derived fields, or needs a fluent API, as in query builders and test data factories. The builder pattern guide shows a full example.

Singleton pattern and its caveats

A Singleton ensures a class has exactly one instance and provides global access to it. It is the most widely known pattern and the one most often regretted:

  1. Hidden dependencies. Code that calls Config.getInstance() does not declare that it needs configuration, which makes it harder to understand.
  2. Hard to test. Global state leaks between tests unless you add reset hooks.
  3. Concurrency hazards. Lazy initialization in multithreaded runtimes needs careful synchronization.
  4. Premature global scope. "There is only one" is often true today and false later, for example when you add multi-tenancy.

A better default is to create one instance at application startup and pass it in through dependency injection. You still get a single instance, without the global access. In Node.js, a module that exports one instance behaves like a singleton already, with the same caveats.

Code: Strategy, Decorator, and Observer in TypeScript

The following example combines three patterns. Shipping cost uses Strategy, a caching Decorator wraps a rate lookup, and an Observer-style event bus announces completed quotes.

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

const standard: ShippingStrategy = (w) => 5 + w * 1.2;
const express: ShippingStrategy = (w) => 12 + w * 2.5;

interface RateSource {
  getRate(region: string): Promise<number>;
}

class CachedRateSource implements RateSource {
  private cache = new Map<string, number>();
  constructor(private readonly inner: RateSource) {}

  async getRate(region: string): Promise<number> {
    const hit = this.cache.get(region);
    if (hit !== undefined) return hit;
    const rate = await this.inner.getRate(region);
    this.cache.set(region, rate);
    return rate;
  }
}

type Listener<T> = (event: T) => void;

class EventBus<T> {
  private listeners = new Set<Listener<T>>();

  subscribe(fn: Listener<T>): () => void {
    this.listeners.add(fn);
    return () => this.listeners.delete(fn);
  }

  publish(event: T): void {
    for (const fn of this.listeners) fn(event);
  }
}

async function quote(
  weightKg: number,
  region: string,
  strategy: ShippingStrategy,
  rates: RateSource,
  bus: EventBus<{ region: string; total: number }>,
): Promise<number> {
  const total = strategy(weightKg) * (await rates.getRate(region));
  bus.publish({ region, total });
  return total;
}

Each piece can change independently. A new shipping tier is one more function. Removing the cache means passing the inner source directly. Adding analytics means one more subscriber.

How to choose the right design pattern

Start from the problem, not the catalog. Ask what is varying or what is painful:

  • The algorithm varies: Strategy.
  • The concrete type to create varies: Factory.
  • Many parts must react to one change: Observer.
  • You need optional, stackable behavior: Decorator.
  • An external API does not match yours: Adapter.
  • Construction is long, optional-heavy, or order-dependent: Builder.

If nothing is varying and nothing hurts, you probably do not need a pattern yet. Patterns applied without a problem add indirection with no payoff.

Key takeaways

  • Patterns are named solutions to recurring problems; their biggest value is shared vocabulary.
  • Strategy, Factory, Observer, Decorator, Adapter, and Builder cover most everyday design needs.
  • Decorator keeps an interface and adds behavior; Adapter changes the interface.
  • Prefer injecting a single instance over a global Singleton.
  • Introduce a pattern when you feel real variation or pain, not in anticipation of it.

Frequently asked questions

Which design patterns are most important to learn first?

Strategy, Observer, Factory, and Decorator are the most broadly useful because they appear in application code, frameworks, and interviews alike. Adapter and Builder come next. Learn the problem each solves rather than memorizing class diagrams.

Is Singleton an anti-pattern?

Not inherently, but it is frequently misused. A global Singleton hides dependencies and complicates testing. Having exactly one instance is often fine; the problem is global access, which dependency injection avoids.

What is the difference between Strategy and State patterns?

Both delegate behavior to an interchangeable object. In Strategy, the client chooses the algorithm and it usually stays fixed for the operation. In State, the object changes its own behavior as its internal state transitions, and the state objects often trigger those transitions themselves.

Are design patterns still relevant in modern TypeScript?

Yes, though many become lighter. First-class functions make Strategy and Observer simple, and structural typing makes Adapter and Decorator easy to write. The underlying design problems have not gone away, so the vocabulary remains useful.