Skip to main content

Design patterns — four archetypes and their vocabulary

The value of design patterns lies less in being implementation templates and more in becoming a shared vocabulary. Say "we'll do this with a Strategy", and the design of factoring out swappable behavior lands in a single word.

Meanwhile, remembering only the names while getting the substance wrong makes conversations miss each other. This chapter takes four representative patterns and organizes them by the problem each was solving, along with commonly conflated alternatives.

What you'll learn in this chapter​

  • What Strategy, Singleton, Observer, and Abstract Factory each solve
  • The differences between Strategy and State, and between Observer and polling
  • The relationship between Abstract Factory and Factory Method
Prerequisites

Experience writing classes and interfaces is enough. Code examples are in TypeScript.

What this chapter does not cover​

How each pattern looks in TypeScript, and whether it's even needed in modern TypeScript, is covered by a dedicated guide.

  • TypeScript Design Patterns — All 16 chapters revisiting the 23 GoF patterns in modern TypeScript
  • Summary of judgments — Table judging each pattern as "language-feature replaceable / conditional / class implementation recommended"

This chapter's job stops at aligning the vocabulary. Read the guide for the adoption decisions.

Strategy — make the algorithm swappable​

Problem: the same purpose has several ways of doing it, and you want to pick between them at runtime. Writing it as branches makes the branch structure grow every time a way is added.

Take the algorithm (strategy) as a value passed in from outside, so callers can swap it.

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

const standard: ShippingStrategy = (w) => 500 + w * 100;
const express: ShippingStrategy = (w) => 1200 + w * 150;
const freeOverLimit: ShippingStrategy = (w) => (w > 10 ? 0 : 500);

function checkout(weightKg: number, calcShipping: ShippingStrategy): number {
return calcShipping(weightKg);
}

checkout(5, express); // 1950

checkout doesn't change when more shipping methods are added. "What to do" is fixed, and "how to do it" is swapped. That's Strategy.

Difference from State​

The most commonly conflated pattern is State.

StrategyState
What's swappedAlgorithmBehavior per state of an object
Who picksExternal callerThe object itself (switched by transitions)
Concept of transitionNoneYes

If what you can do depends on the current state, and that state itself moves through transitions, it's State. Strategy has no notion of transition. It just uses the strategy handed in from outside.

An order moving through "received → shipped → delivered", with the available operations changing at each state, is State. Choosing among ways to compute a shipping fee is Strategy.

Singleton — limit to one instance​

Problem: things like configuration or a connection pool should exist only once in the whole system. Multiple instances leave state inconsistent.

Guarantee only one instance of the type exists, and consolidate the access point.

class AppConfig {
private static instance: AppConfig | null = null;
private constructor(readonly apiBaseUrl: string) {}

static getInstance(): AppConfig {
if (AppConfig.instance === null) {
AppConfig.instance = new AppConfig(process.env.API_BASE_URL ?? '');
}
return AppConfig.instance;
}
}

AppConfig.getInstance() === AppConfig.getInstance(); // true

Making the constructor uncallable from outside and narrowing acquisition to one method is the base form.

Singleton also has the side of "reach for the same thing from anywhere". That's convenience, but it also creates state anyone can write from anywhere. The critiques — hard to swap in tests, dependencies become invisible — come from that.

Also, in JavaScript and TypeScript, modules are evaluated once, giving many situations where you can get the same effect without writing a class. The dividing line is covered in the dedicated guide's Singleton chapter.

Observer — notify subscribers when state changes​

Problem: you want to inform multiple interested parties when some subject's state changes. Having the subject directly know about them means edits every time the set grows or shrinks.

The subject holds a list of subscribers (observers), and the subject side pushes the notification the moment the change happens.

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

class Store<T> {
private listeners: Listener<T>[] = [];

constructor(private value: T) {}

subscribe(listener: Listener<T>): () => void {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter((l) => l !== listener);
};
}

set(next: T): void {
this.value = next;
// Notify at the moment of the change
this.listeners.forEach((l) => l(next));
}
}

const cart = new Store(0);
const unsubscribe = cart.subscribe((count) => console.log(`Cart: ${count} items`));
cart.set(3); // "Cart: 3 items"
unsubscribe();

Difference from polling​

Observer is not "check at intervals". This is where people misread it the most.

Observer (push)Polling (pull)
TriggerThe moment of changeA regular timer
Wasted workNoneRuns every tick even without change
DelayNearly zeroUp to one interval
ImplementationSubscription list and notificationRepeated check via setInterval, etc.

Continuously querying at intervals to detect change is polling. Observer is the reverse — the side that changed sends the notice — so the polling itself becomes unnecessary. That's the whole reason to use Observer. Events the browser raises from outside, such as user input, are queued and processed as capacity allows (Event loop). When your own code sends the notice — the Store above, or the DOM's dispatchEvent — subscribers are called synchronously, right there.

The browser's addEventListener, Node.js's EventEmitter, and the subscription mechanisms of state-management libraries all follow this shape.

Problem: you have a set of objects that must be used together, and mixing families breaks things. And you don't want the caller to name the concrete types.

Let the caller build a family of parts through a single door, without naming the concrete types, keeping the combinations consistent.

interface Button { render(): string }
interface Checkbox { render(): string }

// A single door that builds the whole family
interface UiFactory {
createButton(): Button;
createCheckbox(): Checkbox;
}

const lightFactory: UiFactory = {
createButton: () => ({render: () => '<button class="light">'}),
createCheckbox: () => ({render: () => '<input class="light">'}),
};

const darkFactory: UiFactory = {
createButton: () => ({render: () => '<button class="dark">'}),
createCheckbox: () => ({render: () => '<input class="dark">'}),
};

// The caller doesn't know the concretes. light and dark can never mix
function buildForm(factory: UiFactory): string {
return factory.createButton().render() + factory.createCheckbox().render();
}

The caller picks lightFactory or darkFactory in one place. The inconsistency of a light-themed button next to a dark-themed checkbox becomes structurally impossible. That's the value of handling "a related family" together.

Difference from Factory Method​

Factory MethodAbstract Factory
What it buildsOne kind of objectA family of related kinds
Main aimHide creation detailsKeep the combination consistent

Factory Method is "one method that builds a button"; Abstract Factory is "one door that can build a button, a checkbox, and an input".

Commonly conflated pairings​

DescriptionThe right pattern
Available operations change based on current stateState (not Strategy)
Detect changes by asking at intervalsPolling (not Observer)
Hide the creation of one kind of objectFactory Method (not Abstract Factory)
Swap the algorithm from outsideStrategy
The side that changed notifies subscribersObserver
Build a related family through one doorAbstract Factory
Limit to one instanceSingleton

Common misconceptions​

"Strategy and State are the same." — Class diagrams look almost identical, but the intent differs. State has "which state comes next"; Strategy doesn't. In Strategy, the decision to swap always sits with the caller.

"Observer is a mechanism that continually watches for changes." — The opposite of what the name suggests. Subscribers don't watch. The side that changed sends notifications, so subscribers just wait.

"Abstract Factory is a pattern for building one object." — One kind is Factory Method. Abstract Factory handles a family of related kinds.

"More patterns is better design." — Patterns are means, not ends. If a language feature suffices, that's simpler. See the dedicated guide for judgments.

Exercises​

Q1. Add both Slack and email support to a notification feature. Of the two designs below, which is Abstract Factory and what is the other?

// Option A
interface NotifierFactory {
createSender(): Sender;
}

// Option B
interface ChannelFactory {
createSender(): Sender;
createFormatter(): Formatter;
createRetryPolicy(): RetryPolicy;
}
Answer and explanation

Answer: Option B is Abstract Factory; Option A is Factory Method

The divide is one kind or a related family.

Option A builds only Sender. It builds one kind, so this is the Factory Method side.

Option B builds the three of sending, formatting, and retry. A Slack factory produces the Slack trio; an email factory produces the email trio — combining Slack's sender with the email formatter is unbuildable. This "guaranteed consistency" is the aim of Abstract Factory.

If you later want to narrow the factory itself to one per app, that's when Singleton enters. Abstract Factory and Singleton don't compete; they stack.

Q2. "Let the user choose among credit card, convenience store, and bank transfer" — which pattern?

Answer and explanation

Answer: Strategy

Payment processing has the same goal with multiple ways, chosen at runtime by the caller. No transitions. The picked method is used as-is.

type PaymentMethod = (amountYen: number) => Promise<void>;

async function pay(amountYen: number, method: PaymentMethod): Promise<void> {
await method(amountYen);
}

If instead this were "the order transitions through received → paid → shipped, and each state has different available operations", it's State. External choice vs. automatic transition is the deciding line.

Q3. Someone describes an implementation that "polls the API every second to detect inventory changes" as an Observer pattern. Is that valid?

Answer and explanation

Answer: Not valid. That's polling.

Observer is a mechanism where the side that changed pushes to subscribers. This implementation queries at intervals from the subscriber side — the direction is reversed.

This implementationObserver
TriggerTimerThe moment inventory changes
When nothing changedStill queriesNothing happens
DelayUp to 1 secondNearly zero

To push server-side changes to the client, use WebSocket or Server-Sent Events. Those push notifications, giving an Observer-shaped structure.

Polling isn't bad. It's simple to implement and doesn't require maintaining a connection. But calling it Observer makes design discussions miss each other.

Summary​

  • Strategy swaps the algorithm from outside. It's not State — which handles transitions
  • Singleton limits to one instance and consolidates the access point
  • Observer is push — the changed side notifies subscribers. Opposite direction from polling
  • Abstract Factory builds a family of related parts through one door. One kind is Factory Method
  • Patterns are shared vocabulary. See TypeScript Design Patterns for adoption judgments
  • TCP/IP — On to Part 5. Descending into the networking layer