Strategy
Swapping an algorithm at runtime by passing an object.
01The intuition
The problem it solves
A shop program computes the discount on a bill. On day one there is one rule. By the end of the month there are five: a festival discount, a student card, a bulk rule, a coupon, a loyalty points rule. The obvious code is one function with an if-else ladder, and every new rule means editing the function that already works and retesting all of it.
def discount(bill: float, kind: str, extra: dict) -> float:
if kind == "festival":
return bill * 0.10
elif kind == "student":
return min(bill * 0.15, 200)
elif kind == "bulk":
return 50 if extra["items"] >= 10 else 0
elif kind == "coupon":
return extra["coupon_value"]
# next week: loyalty points ... and the function grows again
return 0
The pain shows when two rules need different inputs (one needs the item count, one needs a coupon code), when the same ladder is copied into the invoice printer, and when a tester asks for a test of the student rule alone.
Build it one piece at a time
Step one: write down the one question every rule answers, as a tiny interface. Every rule takes a bill and returns a discount; nothing else is shared.
from typing import Protocol
class DiscountRule(Protocol):
def apply(self, bill: "Bill") -> float: ...
Step two: move each branch of the ladder into its own small class that satisfies that interface. Each class carries the data only it needs, so the extra dictionary disappears.
class Festival:
def apply(self, bill: "Bill") -> float:
return bill.total * 0.10
class StudentCard:
def apply(self, bill: "Bill") -> float:
return min(bill.total * 0.15, 200)
class Bulk:
def __init__(self, min_items: int, off: float) -> None:
self.min_items, self.off = min_items, off
def apply(self, bill: "Bill") -> float:
return self.off if bill.item_count >= self.min_items else 0
Step three: the checkout no longer decides which rule to use; it is handed one. Choosing happens once, at the edge of the program, and the code in the middle just calls apply. Programmers call the swappable object a strategy.
class Checkout:
def __init__(self, rule: DiscountRule) -> None:
self.rule = rule
def pay(self, bill: "Bill") -> float:
return bill.total - self.rule.apply(bill)
rule = StudentCard() if customer.is_student else Bulk(10, 50)
print(Checkout(rule).pay(bill))
Why it works
The principle: separate the part that stays the same (computing a final price) from the part that changes (how the discount is found), and let the changing part be an object you pass in. The invariant is that Checkout works with any object that honours apply, so adding a rule touches no existing class. Where it breaks: a strategy that reaches back into the checkout to change its state, or a strategy interface so wide that half the rules leave methods empty. Interviewers probe by asking you to add a rule mid-interview; if you open the checkout class to do it, you have not finished the refactor.
The trade-off you must say out loud
Five rules become five classes plus an interface, and someone must still choose the rule somewhere; the ladder has moved, not vanished. For two stable rules, an if is fine and clearer. Say that choosing is a separate responsibility (a factory or a registry) and that strategies should be stateless so one instance can be shared. The follow-up is nearly always "what if several rules apply at once?", which leads to composing strategies or to the specification pattern.
Where it shows up in the 100 Google problems
- Cache manager with pluggable eviction: LRU, LFU and FIFO are eviction strategies injected into one cache class.
- Rate limiter class: token bucket and sliding window are two strategies behind the same isAllowed call.
- Parking lot: the spot-allocation policy (nearest, cheapest, by size) is a strategy the lot is given.
- Splitwise: equal, exact and percentage splits are strategies that each return a list of shares.
- Coupon and discount system: the example above, grown up, with stacking rules on top.
02Used in these problems
- Design a cache manager with pluggable eviction and TTL
- Design a rate limiter class
- Design a billing-token rate limiter for AI agents
- Design a customer-support agent dispatcher
- Design an extensible spreadsheet aggregation API
- Merge two configuration trees with per-node rules
- Design a parking lot
- Design an elevator controller
- Design a multi-elevator dispatcher
- Design an airline reservation system
- Design a car rental system
- Design a student-to-room allocation system
- Design a train platform management system
- Design Snake and Ladder
- Design a deck of cards and Blackjack
- Design an object-oriented poker game
- Design a game matchmaking system
- Design Splitwise
- Design a payment processor
- Design an online stock brokerage
- Design an online shopping system
- Design a food delivery system
- Design a coupon and discount system
- Design a logging framework
- Design a notification service
- Design a URL shortener (class level)
- Design a music player
- Design an in-process search index