InterviewKata
Design patterns · 26 of 26 · Beyond the book

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

A doctor doesn't start from the medicine cabinet. They start from symptoms, narrow down the cause, and sometimes prescribe nothing but rest. A good diagnosis also knows which treatments combine well and which interact badly.

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 codeReach forIn Pedal
The same if kind == chain decides which class to create, in several placesFactory method or a simple factoryDocks releasing bikes, e-bikes, scooters
Objects must come in matching setsAbstract factoryA city's lock, terminal and sign
A constructor with many optional, order-sensitive partsBuilderRide receipts
New objects are small variations of a configured onePrototypeFare plans
An interface you can't change doesn't fit the one you needAdapterPayment provider SDKs
Optional behaviours (logging, caching, retries) to stack around an objectDecoratorThe fare service
Callers repeat a multi-step sequence across a subsystemFacadestart_rental()
Access to an object needs controlling: cache, permission, lazinessProxyDock availability, force-unlock
Code type-checks to treat items and groups differentlyCompositeCity, zones, stations
Classes multiply as a product of two dimensionsBridgeAlert kinds × channels
Thousands of objects repeat the same heavy dataFlyweight40,000 bikes, 3 models
An if/elif chain picks one of several algorithmsStrategyPricing plans
Booleans describe what an object may do nextStateBike lifecycle
Copies of a procedure differ in a few stepsTemplate methodCity reports
Requests must be undone, queued or auditedCommandOperator console
A list of checks, configured per case, any of which can stop a requestChain of responsibilityUnlock checks
A class calls every interested party by nameObserverRide-ended listeners
A web of objects calling each other to coordinateMediatorStation controller
Loops depend on how a collection is storedIteratorPaged trip history
New operations over stable types keep editing those typesVisitorFleet export and maintenance
Undo needs to restore state without exposing itMementoRoute plan
Non-developers need to write rules from a few building blocksInterpreterPromo rules
Classes create their own dependencies, so tests can't swap themDependency injectionRental service
Related changes are saved separately and can fail halfwayRepository and unit of workRides 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.

CombinationWhy they go togetherIn Pedal
Strategy + factoryStrategies need choosing; a factory or registry chooses by key.The rider's plan name picks a Pricing
Composite + iterator or visitorA tree needs walking, and operations over its nodes.Reports over the city's zones and stations
Command + mementoCommands give undo; mementos give them the state to restore.Operator actions on a route plan
Decorator + proxySame wrapping shape: add behaviour, then control access.Logging around a cached availability
Observer + mediatorEvents announce; a mediator coordinates the response.Dock events into the station controller
Adapter + facadeMake each SDK fit, then offer one simple call over them.Payment adapters under start_rental()
Dependency injection + almost everythingStrategies, 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 sightA plain function or class
    The language already does itIterators, decorators, first-class functions
    The framework already does itMiddleware, dependency injection containers, ORM sessions
    The problem is across servicesArchitecture patterns (the Styles and System design tracks)

    8. How choosing goes wrong

    Pattern first, problem second"Let's use the visitor pattern" before anyone has named the change it would make cheap.
    Choosing by the name in the codeA class called Manager doesn't need a mediator. Look at how objects depend on each other.
    Abstracting before the second caseOne payment provider doesn't need an adapter layer yet. Add it when the second provider is signed.
    Never removing a patternWhen the variation goes away (one city left, one plan), collapse the pattern back to plain code.

    9. Remember it and say it

    Memory hook: "Diagnose, then prescribe"Start from the symptom in the code, not from the pattern catalogue; sometimes the answer is nothing.
    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 surgeryThe code
    SymptomsCopied if-chains, edits in five places, untestable classes
    DiagnosisWhich group: making, fitting, choosing, talking
    PrescriptionOne pattern from the symptom table
    Rest and fluidsNo pattern yet

    Watch it on an example

    1. The symptom: adding a pricing plan edits charge() every time.
    2. The group: choosing behaviour. The table says strategy.
    3. 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

    10. Check yourself

    What should come first when choosing a pattern?

    Patterns make specific changes cheap.

    An if/elif chain picks a pricing algorithm, with a fourth plan coming. Which pattern?

    Choosing behaviour, one algorithm at a time.

    A class calls billing, map and rebalancing by name when a ride ends. Which pattern?

    Let them subscribe instead.

    There's one payment provider and no second planned. Adapter layer?

    A pattern needs its problem.

    Strategies need choosing by a plan name. Which pattern pairs well?

    A common combination.

    11. Go deeper

    ResourceWhy
    Which pattern solves whatThe whole catalogue in one table.
    Low-level design trackInterview problems where you choose patterns under time pressure.
    Architecture styles 16: Choosing a styleThe same diagnosis, one level up.
    Martin Fowler: YAGNIWhy not to build for change that hasn't arrived.