Choosing a pattern
The last page turns the catalogue around: start from what hurts in the code, find the pattern that makes that change cheap, and check that the problem is real before building anything. With a symptom table, common combinations, and one ride passing through four patterns at once.
1. Two ways to go wrong
Pattern trouble comes in two opposite kinds. Too little: the same if kind == … chain in five functions, paging loops copied into three screens, a ride service that every team edits. Too much: a PricingStrategyFactoryProvider wrapping the one pricing rule that has never changed, three layers of indirection for something a function would do.
Both come from choosing a pattern by its name rather than by the problem in front of you. This page turns the track around: start from the symptom in the code, find the pattern that answers it, and check whether the problem is real yet.
2. Choosing in one picture
Start with the kind of pain. Is it about making objects (which class, how assembled), fitting objects together (interfaces that don't match, behaviour to wrap around, structures to treat alike), choosing behaviour (an algorithm or state that varies, requests to queue or chain), or objects talking (who needs to know, who coordinates, how to walk or extend a structure)? Then pick within that group using the symptom table below.
And before any of it, ask whether the second case exists yet. A pattern earns its place when there's real variation or change to absorb. One pricing rule needs a function; the third one might need a strategy.
3. Symptom to pattern
| Symptom in the code | Reach for | In Pedal |
|---|---|---|
The same if kind == chain decides which class to create, in several places | Factory method or a simple factory | Docks releasing bikes, e-bikes, scooters |
| Objects must come in matching sets | Abstract factory | A city's lock, terminal and sign |
| A constructor with many optional, order-sensitive parts | Builder | Ride receipts |
| New objects are small variations of a configured one | Prototype | Fare plans |
| An interface you can't change doesn't fit the one you need | Adapter | Payment provider SDKs |
| Optional behaviours (logging, caching, retries) to stack around an object | Decorator | The fare service |
| Callers repeat a multi-step sequence across a subsystem | Facade | start_rental() |
| Access to an object needs controlling: cache, permission, laziness | Proxy | Dock availability, force-unlock |
| Code type-checks to treat items and groups differently | Composite | City, zones, stations |
| Classes multiply as a product of two dimensions | Bridge | Alert kinds × channels |
| Thousands of objects repeat the same heavy data | Flyweight | 40,000 bikes, 3 models |
| An if/elif chain picks one of several algorithms | Strategy | Pricing plans |
| Booleans describe what an object may do next | State | Bike lifecycle |
| Copies of a procedure differ in a few steps | Template method | City reports |
| Requests must be undone, queued or audited | Command | Operator console |
| A list of checks, configured per case, any of which can stop a request | Chain of responsibility | Unlock checks |
| A class calls every interested party by name | Observer | Ride-ended listeners |
| A web of objects calling each other to coordinate | Mediator | Station controller |
| Loops depend on how a collection is stored | Iterator | Paged trip history |
| New operations over stable types keep editing those types | Visitor | Fleet export and maintenance |
| Undo needs to restore state without exposing it | Memento | Route plan |
| Non-developers need to write rules from a few building blocks | Interpreter | Promo rules |
| Classes create their own dependencies, so tests can't swap them | Dependency injection | Rental service |
| Related changes are saved separately and can fail halfway | Repository and unit of work | Rides and payments |
4. Patterns that work together
Real designs combine patterns. These pairs come up again and again; each pair solves two problems that tend to appear together.
| Combination | Why they go together | In Pedal |
|---|---|---|
| Strategy + factory | Strategies need choosing; a factory or registry chooses by key. | The rider's plan name picks a Pricing |
| Composite + iterator or visitor | A tree needs walking, and operations over its nodes. | Reports over the city's zones and stations |
| Command + memento | Commands give undo; mementos give them the state to restore. | Operator actions on a route plan |
| Decorator + proxy | Same wrapping shape: add behaviour, then control access. | Logging around a cached availability |
| Observer + mediator | Events announce; a mediator coordinates the response. | Dock events into the station controller |
| Adapter + facade | Make each SDK fit, then offer one simple call over them. | Payment adapters under start_rental() |
| Dependency injection + almost everything | Strategies, adapters and repositories are all injected. | The composition root wires the whole app |
5. One ride, four patterns
One small program where a simple factory makes the vehicle, strategies price two rides, a decorator logs every price and observers react to each finished ride. Each line is tagged with the pattern that produced it.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Callable
# [simple factory] which class to create, decided in one place
class Vehicle:
def __init__(self, vid: str, kind: str) -> None:
self.vid, self.kind = vid, kind
def make_vehicle(kind: str, vid: str) -> Vehicle:
if kind not in ("bike", "e-bike"):
raise ValueError(f"unknown vehicle kind '{kind}'")
return Vehicle(vid, kind)
# [strategy] the pricing rule is swappable
class Pricing(ABC):
name = ""
@abstractmethod
def price(self, minutes: int) -> int: ...
class Member(Pricing):
name = "member"
def price(self, minutes: int) -> int: return max(0, minutes - 30) * 10
class PayAsYouGo(Pricing):
name = "pay-as-you-go"
def price(self, minutes: int) -> int: return 100 + minutes * 10
# [decorator] logging wrapped around any pricing, same interface
class Logged(Pricing):
def __init__(self, inner: Pricing, log: list[str]) -> None:
self.inner, self.log, self.name = inner, log, inner.name
def price(self, minutes: int) -> int:
p = self.inner.price(minutes)
self.log.append(f"{self.name} {minutes} min -> {p}p")
return p
# [observer] whoever cares about a finished ride subscribes
class Rides:
def __init__(self) -> None:
self.listeners: list[Callable[[str, int], str]] = []
def end(self, rider: str, pence: int) -> list[str]:
return [fn(rider, pence) for fn in self.listeners]
if __name__ == "__main__":
log: list[str] = []
rides = Rides()
rides.listeners.append(lambda rider, p: f"billing: charged {rider} {p}p")
rides.listeners.append(lambda rider, p: "map: S-31 +1 bike")
bike = make_vehicle("e-bike", "E-205")
print(f"[factory] dock D7 released {bike.kind} {bike.vid}")
for rider, pricing, minutes in [("mia", Member(), 42), ("leo", PayAsYouGo(), 18)]:
pence = Logged(pricing, log).price(minutes)
print(f"[strategy] {rider}: {pricing.name}, {minutes} min -> {pence}p")
print(f"[observer] " + " | ".join(rides.end(rider, pence)))
print(f"[decorator] log: " + "; ".join(log))
// [simple factory] which class to create, decided in one place
class Vehicle { constructor(readonly vid: string, readonly kind: string) {} }
function makeVehicle(kind: string, vid: string): Vehicle {
if (kind !== "bike" && kind !== "e-bike") throw new Error(`unknown vehicle kind '${kind}'`);
return new Vehicle(vid, kind);
}
// [strategy] the pricing rule is swappable
interface Pricing { readonly name: string; price(minutes: number): number; }
class Member implements Pricing { readonly name = "member"; price(m: number) { return Math.max(0, m - 30) * 10; } }
class PayAsYouGo implements Pricing { readonly name = "pay-as-you-go"; price(m: number) { return 100 + m * 10; } }
// [decorator] logging wrapped around any pricing, same interface
class Logged implements Pricing {
readonly name: string;
constructor(private readonly inner: Pricing, private readonly log: string[]) { this.name = inner.name; }
price(minutes: number): number {
const p = this.inner.price(minutes);
this.log.push(`${this.name} ${minutes} min -> ${p}p`);
return p;
}
}
// [observer] whoever cares about a finished ride subscribes
class Rides {
readonly listeners: ((rider: string, pence: number) => string)[] = [];
end(rider: string, pence: number): string[] { return this.listeners.map((fn) => fn(rider, pence)); }
}
const log: string[] = [];
const rides = new Rides();
rides.listeners.push((rider, p) => `billing: charged ${rider} ${p}p`);
rides.listeners.push(() => "map: S-31 +1 bike");
const bike = makeVehicle("e-bike", "E-205");
console.log(`[factory] dock D7 released ${bike.kind} ${bike.vid}`);
const trips: [string, Pricing, number][] = [["mia", new Member(), 42], ["leo", new PayAsYouGo(), 18]];
for (const [rider, pricing, minutes] of trips) {
const pence = new Logged(pricing, log).price(minutes);
console.log(`[strategy] ${rider}: ${pricing.name}, ${minutes} min -> ${pence}p`);
console.log("[observer] " + rides.end(rider, pence).join(" | "));
}
console.log("[decorator] log: " + log.join("; "));import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
public class Main {
// [simple factory] which class to create, decided in one place
record Vehicle(String vid, String kind) {}
static Vehicle makeVehicle(String kind, String vid) {
if (!kind.equals("bike") && !kind.equals("e-bike")) throw new IllegalArgumentException("unknown vehicle kind '" + kind + "'");
return new Vehicle(vid, kind);
}
// [strategy] the pricing rule is swappable
interface Pricing { String name(); int price(int minutes); }
static class Member implements Pricing { public String name() { return "member"; } public int price(int m) { return Math.max(0, m - 30) * 10; } }
static class PayAsYouGo implements Pricing { public String name() { return "pay-as-you-go"; } public int price(int m) { return 100 + m * 10; } }
// [decorator] logging wrapped around any pricing, same interface
static class Logged implements Pricing {
private final Pricing inner;
private final List<String> log;
Logged(Pricing inner, List<String> log) { this.inner = inner; this.log = log; }
public String name() { return inner.name(); }
public int price(int minutes) {
int p = inner.price(minutes);
log.add(name() + " " + minutes + " min -> " + p + "p");
return p;
}
}
// [observer] whoever cares about a finished ride subscribes
static class Rides {
final List<BiFunction<String, Integer, String>> listeners = new ArrayList<>();
List<String> end(String rider, int pence) {
List<String> out = new ArrayList<>();
for (BiFunction<String, Integer, String> fn : listeners) out.add(fn.apply(rider, pence));
return out;
}
}
record Trip(String rider, Pricing pricing, int minutes) {}
public static void main(String[] args) {
List<String> log = new ArrayList<>();
Rides rides = new Rides();
rides.listeners.add((rider, p) -> "billing: charged " + rider + " " + p + "p");
rides.listeners.add((rider, p) -> "map: S-31 +1 bike");
Vehicle bike = makeVehicle("e-bike", "E-205");
System.out.println("[factory] dock D7 released " + bike.kind() + " " + bike.vid());
for (Trip t : List.of(new Trip("mia", new Member(), 42), new Trip("leo", new PayAsYouGo(), 18))) {
int pence = new Logged(t.pricing(), log).price(t.minutes());
System.out.println("[strategy] " + t.rider() + ": " + t.pricing().name() + ", " + t.minutes() + " min -> " + pence + "p");
System.out.println("[observer] " + String.join(" | ", rides.end(t.rider(), pence)));
}
System.out.println("[decorator] log: " + String.join("; ", log));
}
}Output (the same in all three languages)
[factory] dock D7 released e-bike E-205 [strategy] mia: member, 42 min -> 120p [observer] billing: charged mia 120p | map: S-31 +1 bike [strategy] leo: pay-as-you-go, 18 min -> 280p [observer] billing: charged leo 280p | map: S-31 +1 bike [decorator] log: member 42 min -> 120p; pay-as-you-go 18 min -> 280p
6. Pattern finder
Pattern finder
Tick what you see in your code. The finder suggests patterns and asks the question that matters first.
7. When patterns cost more than they save
What you get
- Shared vocabulary: one word explains a structure in a review.
- Proven shapes: known trade-offs, fewer surprises.
- Change made cheap where it happens: new plans, providers, cities.
- Easier testing: seams where fakes slot in.
What you pay
- Indirection: every pattern adds a hop to follow.
- More classes: and more names to learn.
- Wrong guesses are expensive: a pattern built for change that never comes is pure cost.
- Fashion: patterns applied because they're known, not because they're needed.
| If… | Use instead |
|---|---|
| There's one case and no second in sight | A plain function or class |
| The language already does it | Iterators, decorators, first-class functions |
| The framework already does it | Middleware, dependency injection containers, ORM sessions |
| The problem is across services | Architecture patterns (the Styles and System design tracks) |
8. How choosing goes wrong
Manager doesn't need a mediator. Look at how objects depend on each other.9. Remember it and say it
How this hook works: what “Diagnose, then prescribe” means
What the slogan means, phrase by phrase
- “Diagnose”
- Name the pain: what change is expensive?
- “then prescribe”
- Pick the pattern that makes that change cheap.
- “sometimes nothing”
- One case needs no pattern.
Why this picture fits: Good doctors treat what's wrong with the patient, not what's in the medicine cabinet.
The story → the concept
| The surgery | The code |
|---|---|
| Symptoms | Copied if-chains, edits in five places, untestable classes |
| Diagnosis | Which group: making, fitting, choosing, talking |
| Prescription | One pattern from the symptom table |
| Rest and fluids | No pattern yet |
Watch it on an example
- The symptom: adding a pricing plan edits
charge()every time. - The group: choosing behaviour. The table says strategy.
- The check: three plans exist and marketing wants more, so the pattern pays for itself.
The trap it saves you from
Collecting patterns like badges, so simple code becomes a maze.
It still works when…
Yes: the same diagnosis works for architecture styles, with bigger symptoms.
In an interview or code review
- Lead with the change: "New pricing plans arrive monthly, so I'd make pricing a strategy." The reason comes before the name.
- Say what you didn't use, and why: "One notification channel today, so no bridge yet."
- Show combinations: strategies chosen by a factory, injected into the service.
- Be ready to remove one: if the variation disappears, simplify back.
10. Check yourself
What should come first when choosing a pattern?
An if/elif chain picks a pricing algorithm, with a fourth plan coming. Which pattern?
A class calls billing, map and rebalancing by name when a ride ends. Which pattern?
There's one payment provider and no second planned. Adapter layer?
Strategies need choosing by a plan name. Which pattern pairs well?
11. Go deeper
| Resource | Why |
|---|---|
| Which pattern solves what | The whole catalogue in one table. |
| Low-level design track | Interview problems where you choose patterns under time pressure. |
| Architecture styles 16: Choosing a style | The same diagnosis, one level up. |
| Martin Fowler: YAGNI | Why not to build for change that hasn't arrived. |