InterviewKata
Design patterns · 13 of 26 · Choosing behaviour

Strategy

Strategy puts each interchangeable algorithm behind one interface so the code that uses it can be handed any of them, even switch at runtime. This page turns Pedal's pricing plans into strategies, adds peak pricing without editing anything, and builds a cheapest-plan button in one line.

1. The pain

Pedal prices a ride differently for each plan: members ride 30 minutes free then pay 10p a minute; pay-as-you-go riders pay £1 to unlock plus 10p a minute; students pay half the pay-as-you-go price. The first charge() function held all three in an if plan == … chain, next to the code that loads the ride and sends the receipt.

Marketing now wants peak pricing for pay-as-you-go at rush hour, and a "show me the cheapest plan for this ride" button. Each new plan grows the chain inside a function that also does other jobs, so every change means retesting all of it, and nothing else can reuse one plan's rule on its own.

2. The idea in one picture

A sat-nav asks how you want to get there: fastest, shortest, or avoiding tolls. The journey, the map and the screen are the same; only the route-finding rule changes. You can switch rule halfway through, and the maker can add "scenic" next year without redesigning the device.

Strategy puts each interchangeable algorithm in its own object behind one interface, and lets the code that uses it (the context) hold whichever one it's given. Billing asks its Pricing for a price; it doesn't know or care which plan it holds. Plans can be swapped at runtime, compared side by side, and added without editing the others.

In Python and TypeScript a strategy is often just a function, and a dictionary of functions is a perfectly good strategy registry. Use classes when a strategy needs a name, configuration or several methods.

3. The roles

RoleIts jobIn Pedal
StrategyThe interface every algorithm implements.Pricing.price(minutes)
Concrete strategyOne algorithm.Member, PayAsYouGo, Student, PeakPayAsYouGo
ContextHolds a strategy and delegates the varying step to it.Billing
ClientChooses the strategy and hands it to the context.Checkout, from the rider's plan

4. Before and after

Ride pricing both ways (Python sketch; the full program is in section 5).

Without the pattern

def charge(rider, minutes):
    ride = load_ride(rider)
    if rider.plan == "member":
        p = max(0, minutes - 30) * 10
    elif rider.plan == "payg":
        p = 100 + minutes * 10
    elif rider.plan == "student":
        p = (100 + minutes * 10) // 2
    # peak pricing goes here too...
    send_receipt(rider, p)

With it

class Member(Pricing):
    def price(self, minutes):
        return max(0, minutes - 30) * 10

class Billing:
    def __init__(self, pricing):
        self.pricing = pricing

    def charge(self, rider, minutes):
        p = self.pricing.price(minutes)
        send_receipt(rider, p)
The next changeWithout the patternWith it
Add peak pricingA fourth branch in charge(); retest all of itA PeakPayAsYouGo class; nothing else changes
"Show me the cheapest plan"Call charge() with fake riders, or copy the rulesCall price() on each strategy and compare
A rider switches plan mid-monthChange a field and hope every branch reads itbilling.switch(Member())

Each rule is a small, separately testable object, and the code that uses it never changes when a rule is added.

5. The code

Four pricing strategies behind one interface and a Billing context that holds one. Leo switches plan on the same object; the last line compares every plan for a 45-minute ride.

from __future__ import annotations
from abc import ABC, abstractmethod


# ---- The strategy interface: one way to price a ride. ----
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


class Student(Pricing):
    name = "student"

    def price(self, minutes: int) -> int:
        return PayAsYouGo().price(minutes) // 2


class PeakPayAsYouGo(Pricing):        # added later: no other class was edited
    name = "peak pay-as-you-go"

    def price(self, minutes: int) -> int:
        return 100 + minutes * 15


# ---- The context: uses whichever strategy it holds, and can be given another. ----
class Billing:
    def __init__(self, pricing: Pricing) -> None:
        self.pricing = pricing

    def switch(self, pricing: Pricing) -> None:
        self.pricing = pricing

    def charge(self, rider: str, minutes: int) -> str:
        return f"{rider} ({self.pricing.name}), {minutes} min: {self.pricing.price(minutes)}p"


if __name__ == "__main__":
    print(Billing(Member()).charge("mia", 42))
    leo = Billing(PayAsYouGo())
    print(leo.charge("leo", 18))
    print(Billing(Student()).charge("sam", 18))
    leo.switch(Member())                        # same object, new behaviour, chosen at runtime
    print(leo.charge("leo", 18))
    print(Billing(PeakPayAsYouGo()).charge("ana", 18))
    cheapest = min([Member(), PayAsYouGo(), Student()], key=lambda p: p.price(45))
    print(f"cheapest for a 45-minute ride: {cheapest.name} ({cheapest.price(45)}p)")
// ---- The strategy interface: one way to price a ride. ----
interface Pricing {
  readonly name: string;
  price(minutes: number): number;
}

class Member implements Pricing {
  readonly name = "member";
  price(minutes: number) { return Math.max(0, minutes - 30) * 10; }
}

class PayAsYouGo implements Pricing {
  readonly name = "pay-as-you-go";
  price(minutes: number) { return 100 + minutes * 10; }
}

class Student implements Pricing {
  readonly name = "student";
  price(minutes: number) { return Math.floor(new PayAsYouGo().price(minutes) / 2); }
}

class PeakPayAsYouGo implements Pricing {   // added later: no other class was edited
  readonly name = "peak pay-as-you-go";
  price(minutes: number) { return 100 + minutes * 15; }
}

// ---- The context: uses whichever strategy it holds, and can be given another. ----
class Billing {
  constructor(private pricing: Pricing) {}
  switch(pricing: Pricing): void { this.pricing = pricing; }
  charge(rider: string, minutes: number): string {
    return `${rider} (${this.pricing.name}), ${minutes} min: ${this.pricing.price(minutes)}p`;
  }
}

console.log(new Billing(new Member()).charge("mia", 42));
const leo = new Billing(new PayAsYouGo());
console.log(leo.charge("leo", 18));
console.log(new Billing(new Student()).charge("sam", 18));
leo.switch(new Member());   // same object, new behaviour, chosen at runtime
console.log(leo.charge("leo", 18));
console.log(new Billing(new PeakPayAsYouGo()).charge("ana", 18));
const plans: Pricing[] = [new Member(), new PayAsYouGo(), new Student()];
const cheapest = plans.reduce((best, p) => (p.price(45) < best.price(45) ? p : best));
console.log(`cheapest for a 45-minute ride: ${cheapest.name} (${cheapest.price(45)}p)`);
import java.util.Comparator;
import java.util.List;

public class Main {
    // ---- The strategy interface: one way to price a ride. ----
    interface Pricing {
        String name();
        int price(int minutes);
    }

    static class Member implements Pricing {
        public String name() { return "member"; }
        public int price(int minutes) { return Math.max(0, minutes - 30) * 10; }
    }

    static class PayAsYouGo implements Pricing {
        public String name() { return "pay-as-you-go"; }
        public int price(int minutes) { return 100 + minutes * 10; }
    }

    static class Student implements Pricing {
        public String name() { return "student"; }
        public int price(int minutes) { return new PayAsYouGo().price(minutes) / 2; }
    }

    static class PeakPayAsYouGo implements Pricing {   // added later: no other class was edited
        public String name() { return "peak pay-as-you-go"; }
        public int price(int minutes) { return 100 + minutes * 15; }
    }

    // ---- The context: uses whichever strategy it holds, and can be given another. ----
    static class Billing {
        private Pricing pricing;
        Billing(Pricing pricing) { this.pricing = pricing; }
        void switchTo(Pricing pricing) { this.pricing = pricing; }
        String charge(String rider, int minutes) {
            return rider + " (" + pricing.name() + "), " + minutes + " min: " + pricing.price(minutes) + "p";
        }
    }

    public static void main(String[] args) {
        System.out.println(new Billing(new Member()).charge("mia", 42));
        Billing leo = new Billing(new PayAsYouGo());
        System.out.println(leo.charge("leo", 18));
        System.out.println(new Billing(new Student()).charge("sam", 18));
        leo.switchTo(new Member());   // same object, new behaviour, chosen at runtime
        System.out.println(leo.charge("leo", 18));
        System.out.println(new Billing(new PeakPayAsYouGo()).charge("ana", 18));
        Pricing cheapest = List.of(new Member(), new PayAsYouGo(), new Student()).stream()
            .min(Comparator.comparingInt(p -> p.price(45))).orElseThrow();
        System.out.println("cheapest for a 45-minute ride: " + cheapest.name() + " (" + cheapest.price(45) + "p)");
    }
}

Output (the same in all three languages)

mia (member), 42 min: 120p
leo (pay-as-you-go), 18 min: 280p
sam (student), 18 min: 140p
leo (member), 18 min: 0p
ana (peak pay-as-you-go), 18 min: 370p
cheapest for a 45-minute ride: member (150p)

6. Try it

Compare plans for one ride

Because every plan has the same interface, the app can price one ride under all of them and pick the cheapest.

Ride length

7. When not to use it

What you get

  • New algorithms without edits: a class or function per rule.
  • Runtime choice: swap the strategy on the same context.
  • Small, testable rules: each one tested on its own.
  • Comparable: run them side by side, as the cheapest-plan button does.

What you pay

  • More objects: one per algorithm.
  • Clients must choose: someone has to know which strategy fits.
  • A shared interface to agree: if one rule needs extra data, every strategy's signature feels it.
  • Overkill for one fixed rule: a branch or two is simpler.
If…Use instead
The behaviour changes because the object's state changes14 State
Only some steps of a fixed sequence vary15 Template method
Two whole families vary independently11 Bridge
There are two rules and no more comingAn if/else is fine

8. How it goes wrong

A strategy that knows the contextIf Member.price() reaches into Billing for data, the two are coupled again. Pass what it needs.
Choosing the strategy inside the contextIf Billing picks its own strategy with an if/elif, the chain has only moved. Let the caller or a factory choose.
Classes where functions would doA one-method strategy with no state can be a function in Python or TypeScript.
Confusing it with stateStrategies are chosen from outside and don't replace themselves; states change as events happen.

9. Remember it and say it

Memory hook: "Same trip, choose the route"One journey; the routing rule is a swappable part.
How this hook works: what “Same trip, choose the route” means

What the slogan means, phrase by phrase

“Same trip”
The context: Billing.
“choose”
The client picks, and can switch.
“the route”
The strategy: one pricing rule.

Why this picture fits: A sat-nav keeps everything the same except how it chooses the way, and lets you change that mid-journey.

The story → the concept

The sat-navThe code
The deviceBilling
Fastest, shortest, no tollsMember, PayAsYouGo, Student
Changing the setting mid-tripswitch(Member())
A new "scenic" modePeakPayAsYouGo

Watch it on an example

  1. Leo's billing holds pay-as-you-go: 18 minutes costs 280p.
  2. He joins as a member; switch(Member()).
  3. His next 18-minute ride costs 0p. Billing didn't change.

The trap it saves you from

An if/elif chain of rules buried inside a function that does other work.

It still works when…

Yes: sort comparators, compression levels, retry policies and payment methods are all strategies.

In an interview or code review

10. Check yourself

What does a strategy encapsulate?

The context delegates the varying step to it.

Adding peak pricing with strategy changes…

That's the open-closed principle at work.

Who should choose the strategy?

Otherwise the if/elif just moves into the context.

In Python, what's the lightest strategy?

Functions are first-class values.

Strategy vs state: which changes itself as events happen?

Strategies are swapped from outside.

11. Go deeper

ResourceWhy
14 StateSame structure, different trigger.
Low-level design 03b: Open-closed principlePricing plans are the classic open-closed example.
Python sorting: key functionsA strategy passed as a function.
Wikipedia: Strategy patternStructure and history.