Course contents

Strategy — swappable algorithms

Sign in to run this lab and save your progress

Learn

Before you start

Strategy is the most common behavioral pattern in real code. Nearly every "algorithm-shaped" decision — pricing, routing, scheduling, sorting, compressing, retrying — uses it, whether or not it's called "Strategy" out loud.

The one-line version

Strategy puts each algorithm into its own class so callers can swap between them without changing any code around them.

The signal to use Strategy

  • You have if/elif on a "type of operation" — the classic smell (pricing type, retry type, notification channel).
  • The choice is made at runtime, not fixed at code time.
  • The algorithms are independent — no shared state tying them together.
  • You need to test each one on its own.

Any two of these → probably Strategy.

How this connects to OCP + composition

Strategy is a specific use of:

  • Open/Closed Principle — add new behavior by writing a new strategy class, not by editing existing dispatch code.
  • Composition over inheritance — a Cart HAS a strategy (composition), rather than BEING a specific strategy (inheritance).

If you got OCP + composition from earlier modules, Strategy is the named packaging of that habit.

Where Strategy shows up in real code

  • Django Paginator — pass a custom pagination strategy
  • Sort keyssorted(items, key=lambda x: x.date) is Strategy-in-lambda-form
  • Kubernetes scheduler plugins
  • Retry policies in libraries like tenacity
  • Cache eviction — LRU, LFU, FIFO all pluggable
  • Load balancers — RoundRobin, LeastConnections
  • Payment methods — you built this in module 1 lesson 5. That was Strategy.
  • Discount calculators — module 2 lesson 2. Strategy.

Strategy vs its cousins

Pattern What it varies
Strategy The ALGORITHM used for one operation
State (next lesson) The BEHAVIOR based on the object's internal state
Command The ACTION as a first-class object
Observer WHO REACTS when something changes

Strategy is specifically about algorithm choice. If you're NOT choosing between algorithms, use a different pattern.

The interview shape

Any question of the form "you have N different ways to do X — design it" is Strategy. Watch for:

  • "Different pricing tiers"
  • "Different shipping methods"
  • "Different sorting algorithms"
  • "Different rate-limiting approaches"
  • "Different matchmaking algorithms"

Every one has the same Strategy shape. Learn the shape once — apply it everywhere.

The "just pass a function" alternative

In Python you can often replace Strategy with a callable:

cart.set_shipping(lambda w, d: 5 * d)

Cleaner for one-off cases. But you lose:

  • Type checking / interface rules
  • A named identity (a function has no clear name for logging)
  • Per-strategy state (a strategy with rate tables loaded from config)

For interviews, prefer the class-based Strategy — it shows design maturity and is easier to explain. Use lambdas only when the strategy really is just a simple function.

The decision cheat-sheet

Situation Pick
N algorithms for one operation, choice at runtime Strategy
N algorithms fixed at code time Inheritance is fine
Algorithm needs per-instance config Strategy classes with __init__
Simple algorithm, one-off Pass a lambda
Object's behavior CHANGES over its lifetime State (next lesson)
Actions need to be queued / undone / logged Command (lesson 13)

Your task

Strategy is the pattern for "one operation, multiple ways to do it". Each way is its own class. Callers hold ONE algorithm-shaped object at a time and can swap it without changing anything else.

You've already seen mini-Strategy in the OCP lesson (discount types). This lesson goes deeper.

The problem

Your e-commerce app ships orders via three carriers:

  • FlatRate — always ₹50, no matter the weight or distance
  • DistanceBased — ₹5 per km
  • WeightBased — ₹20 flat + ₹10 per kg over 5kg

A checkout page lets the user pick a carrier, then computes the shipping cost:

def shipping_cost(carrier, weight_kg, distance_km):
    if carrier == "flat":
        return 50
    elif carrier == "distance":
        return 5 * distance_km
    elif carrier == "weight":
        return 20 + max(0, weight_kg - 5) * 10
    else:
        raise ValueError(f"unknown carrier {carrier}")

Standard smell. Every new carrier → edit this function. The function knows every carrier's rules. You can't test one carrier on its own.

Questions to ask about the requirements

  1. How many algorithms? 2-3 fixed → maybe just live with it. Many, or growing → Strategy.
  2. Do algorithms take DIFFERENT inputs? (Distance needs km. Flat rate needs nothing.) Yes → Strategy with one shared signature that includes all inputs. Truly different signatures per algorithm → harder.
  3. Are algorithms chosen at RUNTIME or fixed at code time? Runtime (user picks carrier) → Strategy.
  4. Should each algorithm be testable on its own? Usually yes.
  5. Do algorithms need per-instance state (like a carrier with rate tables loaded from config)? Yes → Strategy classes with __init__.

For this problem: 3 algorithms today, more possible (Same-Day, International, Cash-on-Delivery), chosen at runtime by the user, should be testable. Strategy fits.

Different designs, compared

Design A — if/elif on carrier name

The naive version above.

  • Good: No extra layers.
  • Bad: Breaks OCP (module 2 lesson 2). Rules for every carrier live in one place. Testing one carrier means setting up the whole dispatch. Adding a carrier edits every place that dispatches.
  • When it wins: 2 algorithms, never growing.

Design B — subclass a Cart per carrier

class Cart:
    def total(self): ...

class FlatRateCart(Cart):
    def shipping(self): return 50

class DistanceBasedCart(Cart):
    def shipping(self): return 5 * self.distance
  • Good: Uses inheritance, familiar.
  • Bad: Cart shouldn't know about shipping rules. And what if the user changes carrier mid-cart? You'd have to rebuild the cart as a different class. LSP-style fragile.
  • When it wins: Rare — inheriting "the surrounding object" doesn't fit "swap the algorithm at runtime".

Design C — Strategy

class ShippingStrategy(ABC):
    @abstractmethod
    def cost(self, weight_kg, distance_km): ...

class FlatRateStrategy(ShippingStrategy):
    def cost(self, weight_kg, distance_km):
        return 50

class DistanceBasedStrategy(ShippingStrategy):
    def cost(self, weight_kg, distance_km):
        return 5 * distance_km

class WeightBasedStrategy(ShippingStrategy):
    def cost(self, weight_kg, distance_km):
        return 20 + max(0, weight_kg - 5) * 10


class Cart:
    def __init__(self, weight_kg, distance_km):
        self.weight_kg = weight_kg
        self.distance_km = distance_km
        self._shipping = FlatRateStrategy()  # default

    def set_shipping(self, strategy):
        self._shipping = strategy

    def shipping_cost(self):
        return self._shipping.cost(self.weight_kg, self.distance_km)
  • Good: Each carrier is one class. Swap at runtime. Add a new carrier = one class. No if/elif. Each algorithm testable on its own.
  • Bad: More classes upfront. Shared signature can be awkward if algorithms need different inputs.
  • When it wins: Anywhere you'd otherwise if/elif on algorithm choice.

Design D — pass a function

cart.set_shipping(lambda w, d: 5 * d)
  • Good: Very Pythonic for simple cases.
  • Bad: No type checking. No place for per-strategy state (a distance strategy that reads from a config table). Testing and inspecting are harder.
  • When it wins: Small internal tools where the strategy really is just a function.

The locked-in choice: Design C

Reviewing the requirements:

  • Multiple algorithms, growing. → Rules out A.
  • Cart shouldn't know shipping rules. → Rules out B.
  • Need per-strategy state (rate tables, config). → Rules out D (functions can close over state, but it's hidden).

Design C wins. Classic Strategy pattern.

The connection to OCP

Strategy is OCP applied to algorithm choice. You already used this shape in module 2 lesson 2 (discount calculator) — Strategy is the pattern name for what that lesson taught.

Different LLD problems, same shape:

  • Sorting algorithms (Bubble, Merge, Quick)
  • Compression (Gzip, LZ4, Snappy)
  • Payment methods (Credit / UPI / Wallet) — already Strategy in module 1 lesson 5
  • Rate limiters (Token Bucket, Sliding Window)
  • Retry policies (Exponential, Fixed, Fibonacci)
  • Load balancers (RoundRobin, LeastConnections, Random)

Where Strategy shows up

  • Django's Paginator accepts custom pagination strategies
  • Every ORM's query planner
  • Kubernetes scheduler with plug-in scheduling strategies
  • Python's sorted(key=lambda ...) — the key function is a (functional) strategy
  • HTTP client's retry policy configuration

Mechanics

Copy the Design C sketch.

Your task

In main.py build:

  1. Abstract ShippingStrategy(ABC) with cost(self, weight_kg, distance_km) → number.
  2. Three concrete strategies:
    • FlatRateStrategy — always 50
    • DistanceBasedStrategy5 * distance_km
    • WeightBasedStrategy20 + max(0, weight_kg - 5) * 10
  3. Cart class:
    • __init__(self, weight_kg, distance_km) — stores both, defaults self._shipping = FlatRateStrategy()
    • set_shipping(self, strategy) — swap the strategy
    • shipping_cost(self) — returns self._shipping.cost(weight_kg, distance_km)

Structural tests enforce:

  • Cart's shipping_cost doesn't compute anything itself; it just delegates.
  • A new strategy defined inline (e.g. SameDayStrategy) works with Cart without any Cart changes.
  • AST check: no if/elif chain on strategy type in Cart.

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
11-strategy-starter