The most-used pattern in machine-coding rounds, and the one interviewers expect you to reach for the moment a requirement says "the rule may change".
Open any delivery app at checkout and look at the delivery fee. Two people ordering the same dinner from the same restaurant can see different numbers: one pays by distance, one has a membership and pays nothing, and on a campaign weekend everyone pays a flat fee. The checkout is the same screen for all of them. Only the rule that prices the delivery changes.
That is the whole idea of the Strategy pattern: when one job can be done in several ways, put each way in its own small class behind one interface, and let the code that needs the job done hold a strategy without knowing which. It is the pattern behind the pricing in the parking lot, the split rules in Splitwise and the fares in ride sharing, and you have already used it every time you passed a comparator to a sort.
This lesson starts from the code most people write first, shows exactly what goes wrong as it grows, refactors it into the pattern in Python, Java, C++ and JavaScript, and ends with the questions an interviewer asks once you've said the word "strategy".
The bill is the job; UPI, card or cash is the strategy. One job, several interchangeable ways to do it, picked from outside.
The first version: one function that grows#
The first fee rule ships as a function. Then marketing adds a weekend campaign, and a membership team adds free delivery. Each one is a new branch:
# delivery_fee_v1.py: one function, and an elif for every new rule
def delivery_fee(cart_total: int, distance_m: int, plan: str) -> int:
if plan == "standard":
started_km = -(-distance_m // 1000) # whole km, rounded up
return 25 + 8 * max(0, started_km - 2)
elif plan == "weekend-flat":
return 19
elif plan == "member":
return 0 if cart_total >= 199 else 15
else:
raise ValueError(f"unknown fee plan: {plan}")
print(delivery_fee(240, 5400, "standard")) # 57
print(delivery_fee(240, 5400, "member")) # 0// DeliveryFeeV1.java: one method, and an else-if for every new rule
class DeliveryFeeV1 {
static int deliveryFee(int cartTotal, int distanceM, String plan) {
if (plan.equals("standard")) {
int startedKm = (distanceM + 999) / 1000; // whole km, rounded up
return 25 + 8 * Math.max(0, startedKm - 2);
} else if (plan.equals("weekend-flat")) {
return 19;
} else if (plan.equals("member")) {
return cartTotal >= 199 ? 0 : 15;
} else {
throw new IllegalArgumentException("unknown fee plan: " + plan);
}
}
public static void main(String[] args) {
System.out.println(deliveryFee(240, 5400, "standard")); // 57
System.out.println(deliveryFee(240, 5400, "member")); // 0
}
}// delivery_fee_v1.cpp: one function, and an else-if for every new rule
#include <algorithm>
#include <iostream>
#include <stdexcept>
#include <string>
int deliveryFee(int cartTotal, int distanceM, const std::string& plan) {
if (plan == "standard") {
int startedKm = (distanceM + 999) / 1000; // whole km, rounded up
return 25 + 8 * std::max(0, startedKm - 2);
} else if (plan == "weekend-flat") {
return 19;
} else if (plan == "member") {
return cartTotal >= 199 ? 0 : 15;
} else {
throw std::invalid_argument("unknown fee plan: " + plan);
}
}
int main() {
std::cout << deliveryFee(240, 5400, "standard") << "\n"; // 57
std::cout << deliveryFee(240, 5400, "member") << "\n"; // 0
}// deliveryFeeV1.js: one function, and an else-if for every new rule
function deliveryFee(cartTotal, distanceM, plan) {
if (plan === "standard") {
const startedKm = Math.ceil(distanceM / 1000); // whole km, rounded up
return 25 + 8 * Math.max(0, startedKm - 2);
} else if (plan === "weekend-flat") {
return 19;
} else if (plan === "member") {
return cartTotal >= 199 ? 0 : 15;
} else {
throw new Error(`unknown fee plan: ${plan}`);
}
}
console.log(deliveryFee(240, 5400, "standard")); // 57
console.log(deliveryFee(240, 5400, "member")); // 057
0The rules, so the numbers make sense: by distance, 25 rupees covers the first 2 km and every further started km adds 8, so a 5.4 km trip is 6 started km and costs 25 + 8 × 4 = 57. The weekend campaign is a flat 19. Members pay nothing on carts of 199 rupees or more, and a 15-rupee small-cart fee below that. Money is a whole number of rupees, never a float.
Nothing here is wrong today. It runs, and three branches are easy to read. The trouble is what the next six months do to it:
- Every new rule edits the same function. A rain surcharge, a festival rate, a per-city rule: each one opens the file that already prices every order in the app.
- Old rules get retested for a change they didn't need. A typo in the new branch can break the standard fee, so every release retests all of them.
- One function, several teams. Marketing owns the campaign, the membership team owns member pricing, and both are editing the same lines.
- The settings are buried. The 19 and the 199 live inside branches, so changing a campaign amount means a code change and a deploy.
- The choice is remade on every call, with a string the caller can mistype.
Before the refactor: what is the part that changes?
Look at delivery_fee. Which part of it will change most often after launch, and which part will stay the same?
One strong answer
The rule changes; the job doesn't. Every branch does the same job (take an order, return a fee in rupees), and the checkout uses the answer the same way whichever branch ran. The way the fee is worked out is the part that keeps changing. So the job becomes an interface, and each way becomes a class that implements it.
The pattern#
The book that named the pattern (Design Patterns, 1994, by Gamma, Helm, Johnson and Vlissides) states its intent in two sentences: "Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it."
In our checkout that gives three roles, plus one more that the book's diagram leaves out and interviews always ask about:
| Role | In the delivery app | Its one job |
|---|---|---|
| Strategy (the interface) | FeePolicy with fee(order) |
Say what every fee rule can do |
| Concrete strategies | DistanceFee, FlatFee, MemberFee |
Each prices a delivery one way, with its own settings |
| Context | Checkout |
Uses whichever policy it was given; never asks which one |
| The chooser | policy_for(plan) |
Picks the policy once, from config or the user's account |
%% title: Strategy: the delivery fee
classDiagram
class Checkout {
-policy FeePolicy
+total(order) int
}
class FeePolicy {
<<interface>>
+fee(order) int
}
class DistanceFee {
+base int
+free_km int
+per_km int
+fee(order) int
}
class FlatFee {
+amount int
+fee(order) int
}
class MemberFee {
+minimum int
+small_cart_fee int
+fee(order) int
}
class Order {
+cart_total int
+distance_m int
}
Checkout --> FeePolicy : uses
FeePolicy <|.. DistanceFee
FeePolicy <|.. FlatFee
FeePolicy <|.. MemberFee
FeePolicy ..> Order : readsRead the arrows. Checkout points at the interface, not at any rule, so it compiles and runs without knowing a single fee rule exists. The dotted arrows with hollow heads are "implements": each rule promises to answer fee(order). That one arrow from Checkout is the design: the context depends on the abstraction, and the details depend on it too (the dependency inversion principle, and open/closed in practice: open to new rules, closed to edits).
Three roles: the FeePolicy interface, one small class per rule, and a Checkout that holds a policy without asking which. A chooser hands it over once.
The code#
Each rule is a small class with its settings in the constructor. Checkout takes a policy in its constructor and calls it. The registry at the bottom is the one place where a plan name turns into a policy.
# delivery_fee.py: one checkout, three fee rules, each rule in its own class
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass(frozen=True)
class Order:
cart_total: int # rupees, never a float
distance_m: int # metres from the store
class FeePolicy(ABC):
"""The strategy: one job (price the delivery), many ways to do it."""
@abstractmethod
def fee(self, order: Order) -> int: ...
class DistanceFee(FeePolicy):
"""25 rupees covers the first 2 km; every further started km adds 8."""
def __init__(self, base: int = 25, free_km: int = 2, per_km: int = 8) -> None:
self.base, self.free_km, self.per_km = base, free_km, per_km
def fee(self, order: Order) -> int:
started_km = -(-order.distance_m // 1000) # whole km, rounded up
return self.base + self.per_km * max(0, started_km - self.free_km)
class FlatFee(FeePolicy):
def __init__(self, amount: int) -> None:
self.amount = amount
def fee(self, order: Order) -> int:
return self.amount
class MemberFee(FeePolicy):
"""Free on carts of 199 rupees or more; a small-cart fee below that."""
def __init__(self, minimum: int = 199, small_cart_fee: int = 15) -> None:
self.minimum, self.small_cart_fee = minimum, small_cart_fee
def fee(self, order: Order) -> int:
return 0 if order.cart_total >= self.minimum else self.small_cart_fee
class Checkout:
"""The context: it is given a policy and never asks which one it holds."""
def __init__(self, policy: FeePolicy) -> None:
self.policy = policy
def total(self, order: Order) -> int:
return order.cart_total + self.policy.fee(order)
# The one place that chooses. The plan name comes from config or the user's account.
POLICIES = {
"standard": DistanceFee(),
"weekend-flat": FlatFee(19),
"member": MemberFee(),
}
def policy_for(plan: str) -> FeePolicy:
if plan not in POLICIES:
raise ValueError(f"unknown fee plan: {plan}")
return POLICIES[plan]
if __name__ == "__main__":
near = Order(cart_total=150, distance_m=1200)
far = Order(cart_total=240, distance_m=5400)
for plan in ["standard", "weekend-flat", "member"]:
checkout = Checkout(policy_for(plan))
print(f"{plan}: near {checkout.total(near)}, far {checkout.total(far)}")
try:
policy_for("gold")
except ValueError as e:
print("rejected:", e)// DeliveryFee.java: one checkout, three fee rules, each rule in its own class
import java.util.List;
import java.util.Map;
class DeliveryFee {
record Order(int cartTotal, int distanceM) {} // rupees, never a double; metres
/** The strategy: one job (price the delivery), many ways to do it. */
interface FeePolicy {
int fee(Order order);
}
/** 25 rupees covers the first 2 km; every further started km adds 8. */
static final class DistanceFee implements FeePolicy {
private final int base, freeKm, perKm;
DistanceFee(int base, int freeKm, int perKm) { this.base = base; this.freeKm = freeKm; this.perKm = perKm; }
DistanceFee() { this(25, 2, 8); }
public int fee(Order order) {
int startedKm = (order.distanceM() + 999) / 1000; // whole km, rounded up
return base + perKm * Math.max(0, startedKm - freeKm);
}
}
static final class FlatFee implements FeePolicy {
private final int amount;
FlatFee(int amount) { this.amount = amount; }
public int fee(Order order) { return amount; }
}
/** Free on carts of 199 rupees or more; a small-cart fee below that. */
static final class MemberFee implements FeePolicy {
private final int minimum, smallCartFee;
MemberFee(int minimum, int smallCartFee) { this.minimum = minimum; this.smallCartFee = smallCartFee; }
MemberFee() { this(199, 15); }
public int fee(Order order) { return order.cartTotal() >= minimum ? 0 : smallCartFee; }
}
/** The context: it is given a policy and never asks which one it holds. */
static final class Checkout {
private final FeePolicy policy;
Checkout(FeePolicy policy) { this.policy = policy; }
int total(Order order) { return order.cartTotal() + policy.fee(order); }
}
// The one place that chooses. The plan name comes from config or the user's account.
static final Map<String, FeePolicy> POLICIES = Map.of(
"standard", new DistanceFee(),
"weekend-flat", new FlatFee(19),
"member", new MemberFee());
static FeePolicy policyFor(String plan) {
FeePolicy p = POLICIES.get(plan);
if (p == null) throw new IllegalArgumentException("unknown fee plan: " + plan);
return p;
}
public static void main(String[] args) {
Order near = new Order(150, 1200);
Order far = new Order(240, 5400);
for (String plan : List.of("standard", "weekend-flat", "member")) {
Checkout checkout = new Checkout(policyFor(plan));
System.out.println(plan + ": near " + checkout.total(near) + ", far " + checkout.total(far));
}
try {
policyFor("gold");
} catch (IllegalArgumentException e) {
System.out.println("rejected: " + e.getMessage());
}
}
}// delivery_fee.cpp: one checkout, three fee rules, each rule in its own class
#include <algorithm>
#include <iostream>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
struct Order {
int cartTotal; // rupees, never a double
int distanceM; // metres from the store
};
// The strategy: one job (price the delivery), many ways to do it.
class FeePolicy {
public:
virtual ~FeePolicy() = default;
virtual int fee(const Order& order) const = 0;
};
// 25 rupees covers the first 2 km; every further started km adds 8.
class DistanceFee : public FeePolicy {
int base_, freeKm_, perKm_;
public:
DistanceFee(int base = 25, int freeKm = 2, int perKm = 8) : base_(base), freeKm_(freeKm), perKm_(perKm) {}
int fee(const Order& order) const override {
int startedKm = (order.distanceM + 999) / 1000; // whole km, rounded up
return base_ + perKm_ * std::max(0, startedKm - freeKm_);
}
};
class FlatFee : public FeePolicy {
int amount_;
public:
explicit FlatFee(int amount) : amount_(amount) {}
int fee(const Order&) const override { return amount_; }
};
// Free on carts of 199 rupees or more; a small-cart fee below that.
class MemberFee : public FeePolicy {
int minimum_, smallCartFee_;
public:
MemberFee(int minimum = 199, int smallCartFee = 15) : minimum_(minimum), smallCartFee_(smallCartFee) {}
int fee(const Order& order) const override { return order.cartTotal >= minimum_ ? 0 : smallCartFee_; }
};
// The context: it is given a policy and never asks which one it holds.
class Checkout {
const FeePolicy& policy_;
public:
explicit Checkout(const FeePolicy& policy) : policy_(policy) {}
int total(const Order& order) const { return order.cartTotal + policy_.fee(order); }
};
// The one place that chooses. The plan name comes from config or the user's account.
const FeePolicy& policyFor(const std::string& plan) {
static const std::map<std::string, std::shared_ptr<const FeePolicy>> policies = {
{"standard", std::make_shared<DistanceFee>()},
{"weekend-flat", std::make_shared<FlatFee>(19)},
{"member", std::make_shared<MemberFee>()},
};
auto it = policies.find(plan);
if (it == policies.end()) throw std::invalid_argument("unknown fee plan: " + plan);
return *it->second;
}
int main() {
Order near{150, 1200}, far{240, 5400};
for (const std::string plan : {"standard", "weekend-flat", "member"}) {
Checkout checkout(policyFor(plan));
std::cout << plan << ": near " << checkout.total(near) << ", far " << checkout.total(far) << "\n";
}
try {
policyFor("gold");
} catch (const std::invalid_argument& e) {
std::cout << "rejected: " << e.what() << "\n";
}
}// deliveryFee.js: one checkout, three fee rules, each rule in its own class
// An order: { cartTotal, distanceM } (whole rupees, metres from the store)
// The strategy: any object with fee(order). JavaScript has no interfaces,
// so the contract is the method name (and a test for every policy).
/** 25 rupees covers the first 2 km; every further started km adds 8. */
class DistanceFee {
constructor(base = 25, freeKm = 2, perKm = 8) {
Object.assign(this, { base, freeKm, perKm });
}
fee(order) {
const startedKm = Math.ceil(order.distanceM / 1000); // whole km, rounded up
return this.base + this.perKm * Math.max(0, startedKm - this.freeKm);
}
}
class FlatFee {
constructor(amount) {
this.amount = amount;
}
fee(order) {
return this.amount;
}
}
/** Free on carts of 199 rupees or more; a small-cart fee below that. */
class MemberFee {
constructor(minimum = 199, smallCartFee = 15) {
Object.assign(this, { minimum, smallCartFee });
}
fee(order) {
return order.cartTotal >= this.minimum ? 0 : this.smallCartFee;
}
}
/** The context: it is given a policy and never asks which one it holds. */
class Checkout {
constructor(policy) {
this.policy = policy;
}
total(order) {
return order.cartTotal + this.policy.fee(order);
}
}
// The one place that chooses. The plan name comes from config or the user's account.
const POLICIES = new Map([
["standard", new DistanceFee()],
["weekend-flat", new FlatFee(19)],
["member", new MemberFee()],
]);
function policyFor(plan) {
if (!POLICIES.has(plan)) throw new Error(`unknown fee plan: ${plan}`);
return POLICIES.get(plan);
}
const near = { cartTotal: 150, distanceM: 1200 };
const far = { cartTotal: 240, distanceM: 5400 };
for (const plan of ["standard", "weekend-flat", "member"]) {
const checkout = new Checkout(policyFor(plan));
console.log(`${plan}: near ${checkout.total(near)}, far ${checkout.total(far)}`);
}
try {
policyFor("gold");
} catch (e) {
console.log(`rejected: ${e.message}`);
}All four print the same thing:
standard: near 175, far 297
weekend-flat: near 169, far 259
member: near 165, far 240
rejected: unknown fee plan: goldCheck one row by hand. The near order is a 150-rupee cart 1.2 km away: by distance that's 2 started km, inside the free 2, so the fee is the base 25 and the total is 175. For a member the same cart is under 199, so the small-cart fee applies: 150 + 15 = 165.
What each language brings to the same design:
- Python:
ABCand@abstractmethodmake a class that forgetsfeefail when it's created, not halfway through a checkout. (Python would also accept any object with afeemethod, but the base class documents the contract.) - Java: an
interfacewith one method. Because it has exactly one abstract method, Java also lets you pass a lambda wherever aFeePolicyis expected; you'll see that in the next section with a built-in interface. - C++: a class with a pure
virtualfunction and a virtual destructor, so deleting a rule through aFeePolicypointer is safe.Checkoutholds a reference because the registry owns the rules and outlives every checkout. - JavaScript: no interfaces at all, so the contract is the method name. Keep a test per policy, because nothing else checks that a new rule spells
feeright.
Who picks the strategy?#
Here is the part candidates skip, and interviewers ask about. The if-ladder didn't disappear. It moved: from inside the fee calculation, where it ran on every call, to one place at the edge of the system that runs once, when the checkout is built.
Before: inside the calculation
- The checkout decides which rule, on every call
- It knows every rule's name and details
- A new rule edits the checkout
After: once, at the edge
- A registry maps a plan name to a policy
- Config, the user's account or the city picks the name
- A new rule is a new class plus one registry line
That one registry line is a real edit, and it's worth saying so out loud rather than claiming "nothing changes". The point is where the edit lands: in the wiring, which is a list of names, and not in the code that prices orders. The pricing code, and its tests, stay exactly as they were.
The chooser can be as simple or as smart as the business needs: a dictionary lookup (as here), a rule per city loaded from config, or a function of the user and the date. Choosing is a job of its own; when it grows, it becomes a factory (the Factory lesson builds one). The checkout never finds out.
The if doesn't vanish: it moves from the checkout, where it ran on every order, to one registry that picks the policy once.
A rainy-day fee
Operations wants a rule for heavy rain: a flat 40 rupees in the affected areas. In this design, which existing classes change?
One strong answer
None of the fee classes, and not Checkout. You write RainFee (or reuse FlatFee(40), since it's the same shape), register it under a plan name such as "rain", and whatever decides the plan for an area picks it. The only existing line that changes is the registry. If your answer was "add a branch to Checkout", that's the if-ladder coming back.
A strategy can be just a function#
The pattern was written down for languages where the only way to pass behaviour around was an object. In Python and JavaScript, functions are values; in Java, any interface with one abstract method accepts a lambda; in C++, std::function holds any callable. So a one-method strategy is often just a function:
# fee_functions.py: a one-method strategy can be a plain function
from dataclasses import dataclass
from typing import Callable
@dataclass(frozen=True)
class Order:
cart_total: int
distance_m: int
FeeRule = Callable[[Order], int] # any function from an order to rupees
class Checkout:
def __init__(self, fee: FeeRule) -> None:
self.fee = fee
def total(self, order: Order) -> int:
return order.cart_total + self.fee(order)
def weekend_flat(order: Order) -> int:
return 19
order = Order(cart_total=240, distance_m=5400)
print(Checkout(weekend_flat).total(order)) # 259
print(Checkout(lambda o: 0 if o.cart_total >= 199 else 15).total(order)) # 240// FeeFunctions.java: a one-method interface takes a lambda
import java.util.function.ToIntFunction;
class FeeFunctions {
record Order(int cartTotal, int distanceM) {}
static final class Checkout {
private final ToIntFunction<Order> fee; // any function from an order to rupees
Checkout(ToIntFunction<Order> fee) { this.fee = fee; }
int total(Order order) { return order.cartTotal() + fee.applyAsInt(order); }
}
static int weekendFlat(Order order) { return 19; }
public static void main(String[] args) {
Order order = new Order(240, 5400);
System.out.println(new Checkout(FeeFunctions::weekendFlat).total(order)); // 259
System.out.println(new Checkout(o -> o.cartTotal() >= 199 ? 0 : 15).total(order)); // 240
}
}// fee_functions.cpp: std::function holds any callable with the right signature
#include <functional>
#include <iostream>
#include <utility>
struct Order {
int cartTotal;
int distanceM;
};
using FeeRule = std::function<int(const Order&)>; // any function from an order to rupees
class Checkout {
FeeRule fee_;
public:
explicit Checkout(FeeRule fee) : fee_(std::move(fee)) {}
int total(const Order& order) const { return order.cartTotal + fee_(order); }
};
int weekendFlat(const Order&) { return 19; }
int main() {
Order order{240, 5400};
std::cout << Checkout(weekendFlat).total(order) << "\n"; // 259
std::cout << Checkout([](const Order& o) { return o.cartTotal >= 199 ? 0 : 15; }).total(order) << "\n"; // 240
}// feeFunctions.js: functions are values, so a function is a strategy
class Checkout {
constructor(fee) {
this.fee = fee; // any function from an order to rupees
}
total(order) {
return order.cartTotal + this.fee(order);
}
}
const weekendFlat = (order) => 19;
const order = { cartTotal: 240, distanceM: 5400 };
console.log(new Checkout(weekendFlat).total(order)); // 259
console.log(new Checkout((o) => (o.cartTotal >= 199 ? 0 : 15)).total(order)); // 240259
240You use this form every day without calling it a pattern. A sort takes a strategy for ordering: sorted(orders, key=...) in Python, a Comparator in Java, a comparison function for std::sort in C++ and for Array.prototype.sort in JavaScript. The sort algorithm stays the same; the way two items are compared is passed in.
A function is enough when
- The rule is one method with no settings
- It's used in one or two places
- A name like
weekend_flatsays everything
Reach for a class when
- The rule has settings (
FlatFee(19),MemberFee(199, 15)) - It needs more than one method, such as
feeand a receipt label - You want it named in the registry, the logs and the class diagram
In an interview, start with classes: they show up in your class diagram, and they make the roles obvious to the person grading you. Then mention that in production Python or JavaScript a lambda would do for the simplest rules. That one sentence shows you know the pattern is an idea, not a ceremony.
Strategy and its look-alikes#
Several patterns draw the same diagram: a class holding something behind an interface. What separates them is who decides and why it varies.
| Pattern | Looks like Strategy because | The difference |
|---|---|---|
| State | the context delegates to an object behind an interface | The object switches its own state as events arrive (a vending machine goes Idle → HasMoney → Dispensing). A strategy is chosen from outside and never swaps itself. |
| Template Method | it lets one step of an algorithm vary | It varies the step by inheritance: a subclass overrides it. Strategy varies it by composition: you pass an object in, and can change it without a new subclass. |
| Decorator | it implements the same interface as what it wraps | It adds to any rule (a late-night surcharge on top of whichever fee applies). A strategy replaces the rule. |
A plain if |
it picks between behaviours | Fine when there are two fixed cases that will never grow. Strategy earns its place when rules are added, owned or tested separately. |
The first row is the one interviews test most. Compare the vending machine, where each state class handles an event and moves the machine to the next state, with the parking lot, where the pricing strategy is set once in the constructor and never changes itself. The State lesson puts the two side by side.
Strategy and State share a diagram. A strategy is chosen from outside and never swaps itself; a state machine moves itself as events arrive.
When not to use it#
A pattern applied to a rule that never changes is a cost with no return: an interface, a class per branch, a registry, and a reader who has to open four files to learn that the fee is always 30.
- There's one rule and no sign of a second. Write the function. Refactor when the second rule actually arrives; it's a small, safe change, which is the point of this lesson.
- There are two fixed cases that come from the problem itself, not from the business (
ascendingordescending). Anifreads better. - The variants share almost everything and differ in a number.
FlatFee(19)andFlatFee(40)are one class with a setting, not two classes.
The test is the same one the parking lot uses: if a requirement added at minute 75 would make you edit the code that does the job, that job belongs behind an interface. If no such requirement is plausible, an if is the honest design.
Where it shows up in the case studies#
Strategy appears in more of the course's 22 low-level design rounds than any other pattern. Each row is a job with several ways to do it, chosen per object, per rule or per config:
| Case study | The strategy | The ways |
|---|---|---|
| Parking lot | PricingStrategy, SpotAssignmentStrategy |
hourly tariff; nearest smallest fit |
| LRU cache | EvictionPolicy |
LRU, FIFO |
| Rate limiter | RateLimiter |
three algorithms, chosen per rule |
| Elevator | DispatchStrategy |
LookCostDispatch |
| Splitwise | SplitStrategy |
equal, exact, percent |
| Snake and ladder | Dice, EndRule |
dice for play and for tests; end rules |
| Library | FinePolicy |
PerDayFine; tiers and amnesty weeks as new classes |
| Hotel | PricingRule, CancellationPolicy |
festival, corporate, last-minute rules |
| Ride sharing | FareStrategy, MatchingStrategy |
standard fare, wrapped by surge |
| Food delivery | AssignmentStrategy, RefundPolicy |
nearest idle partner, refund rules |
The meeting room scheduler, the online auction, the task scheduler and the key-value store use it too (room selection, bid increments, retry rules, eviction). And the course itself is full of strategies one level up: the rate-limiting algorithms of Week 1, the retry strategies of Week 2 and the cache eviction policies of Week 0 are each a family of interchangeable ways to do one job.
In the interview#
Say the pattern with the reason, in one breath: "The fee rule is what the business changes most, so I'll make it a strategy: Checkout takes a FeePolicy, and a new rule is a new class. The choice of rule happens once, in a registry keyed by plan." That sentence scores more than naming the pattern alone, because it names the part that varies and where the choice went.
Then expect the follow-ups:
- "Add a 20-rupee late-night surcharge, whatever the rule." Not a fourth strategy, and not a branch in each one: a wrapper that takes any policy and adds 20 after 11 pm. That's the Decorator pattern, the same shape as
SurgeFarein ride sharing. - "A rule needs the time of day, and another needs the city." Don't add a parameter to
fee()for each new input; every rule would change. Put what rules may need on the order (or a small pricing context object) and let each rule read what it uses. - "We want to try a new fee rule on some users first." That's the chooser's job, not the checkout's: pick the policy from the user (for example, by a stable hash of the user id), and log which policy priced each order so the results can be compared.
- "How do you test it?" Each rule alone, with a handful of orders, including the edges (exactly 2 km, a cart of exactly 199). Then
Checkoutonce, with a fake policy that returns a fixed fee. - "Can the strategy change halfway through a session?" Yes: give
Checkouta setter, or build a new checkout. If the swap happens because of something that happened to the object, you're describing State, and you should say so.
Common mistakes#
- A strategy for every
if, including branches that will never change. Four files for a rule that is always 30 rupees. - The context checking what it holds:
if isinstance(self.policy, MemberFee)insideCheckout. The if-ladder is back, just spelled differently. - Strategies that reach into the context (
fee(self, checkout)and then reading its private fields). Pass the data a rule needs, not the object that uses the rule. - Shared mutable state in a shared strategy: one registry instance that counts orders, used by every thread at once.
- Claiming "nothing changes" when a rule is added. The registry changes. Say where the edit lands and why that's the safe place.
- Floats for money. Fees are whole rupees (or paise); rounding errors show up on receipts.
Strategy
Q1/4 The delivery app adds a rain fee of 40 rupees. In the strategy design, what is the right change?
Answer & explanation
Answer: B
A new rule is a new class (or a new instance of an existing one) plus one line where policies are chosen. Checkout and the other rules are not edited. A parameter on fee() would change every rule for the sake of one.
Q2/4 After the refactor, where did the if-ladder go?
Answer & explanation
Answer: C
The decision still exists; it moved from the code that prices every order to one place at the edge. That is what makes the pricing code closed to edits while the set of rules stays open.
Q3/4 A vending machine moves from Idle to HasMoney when a coin goes in. Is that Strategy?
Answer & explanation
Answer: B
Strategy and State draw the same diagram. The difference is who changes the behaviour: a strategy is chosen from outside and never swaps itself; a state machine moves itself from state to state.
Q4/4 When is a plain if better than a strategy?
Answer & explanation
Answer: C
The pattern pays off when rules are added, owned or tested separately. For two cases that can't change, such as ascending or descending, an if is clearer, and refactoring later is cheap.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


