Design Patterns, Explained Simply · Behavioral Patterns: How Objects Share Work

Strategy

Define a family of interchangeable algorithms behind one interface and choose one at runtime, so the code that uses them never needs an if/else to pick.

The problem

Shipping cost depends on the method the customer picks: standard (a flat fee, free over 50), express (a base fee plus per-kilogram), or store pickup (free). The shipping_cost() function is an if/elif chain mixed with each rule's details. Marketing wants a "free shipping weekend" rule, a partner wants their own rates, and tests have to go through the whole function to check one rule.

You saw the shape of the fix in the first lesson of this course: each variant becomes its own object.

Real-world analogy

Getting to the airport: you can drive, take the train, or call a taxi. The goal is the same and the interface is the same ("get me there by 10:00"), but the strategy differs. You choose based on budget and time, and you can switch strategy without changing the goal.

Without the pattern

def shipping_cost(method, weight_kg, subtotal):
    if method == "standard":
        return 0 if subtotal >= 5000 else 499
    elif method == "express":
        return 999 + 200 * weight_kg
    elif method == "pickup":
        return 0
    # every new rule edits this function

The pattern

  • Strategy interface: one method for the algorithm (cost(weight, subtotal)).
  • Concrete strategies: one class (or one function) per algorithm.
  • Context (Checkout): holds a strategy and calls it. It never checks which strategy it has.
  • Client: picks the strategy, for example from user input or configuration, often through a dictionary lookup.

In Python and TypeScript, a strategy is often just a function. Java's functional interfaces and lambdas, and PHP closures, do the same; a full class is only needed when the strategy has configuration or several methods.

The code

Interchangeable shipping strategiesTry it yourself
class Standard:
    def cost(self, weight_kg, subtotal):
        return 0 if subtotal >= 5000 else 499


class Express:
    def __init__(self, per_kg):
        self.per_kg = per_kg  # strategies can carry configuration

    def cost(self, weight_kg, subtotal):
        return 999 + self.per_kg * weight_kg


class Pickup:
    def cost(self, weight_kg, subtotal):
        return 0


class Checkout:  # context: uses whatever strategy it is given
    def __init__(self, shipping):
        self.shipping = shipping

    def total(self, weight_kg, subtotal):
        return subtotal + self.shipping.cost(weight_kg, subtotal)


STRATEGIES = {"standard": Standard(), "express": Express(per_kg=200), "pickup": Pickup()}

for choice in ["standard", "express", "pickup"]:
    total = Checkout(STRATEGIES[choice]).total(weight_kg=3, subtotal=4200)
    print(f"{choice:<8} total: {total} cents")

# A strategy can also be a plain function wrapped in a tiny object
weekend = type("Weekend", (), {"cost": lambda self, w, s: 0})()
print(f"{'weekend':<8} total: {Checkout(weekend).total(3, 4200)} cents")

Output

standard total: 4699 cents
express  total: 5799 cents
pickup   total: 4200 cents
weekend  total: 4200 cents

Checkout has no idea how shipping is calculated. Each rule can be tested on its own, and the weekend promotion was added without editing anything.

When to use it

  • You have several ways to do the same thing (pricing, sorting, compression, routing, payment, authentication, retry policies) and want to choose at runtime or by configuration.
  • An if/else or switch picks an algorithm and keeps growing.
  • You want to test each algorithm in isolation.

When not to use it

  • There are only two simple variants that rarely change. An if is fine.
  • In languages with first-class functions, a function parameter is often enough. You don't need an interface plus a class per strategy.
  • Clients must know the strategies to choose between them. If choosing is complex, combine Strategy with a factory or a lookup table.

You've already used it

  • Sorting with comparators: sorted(items, key=...), array.sort((a, b) => ...), Comparator in Java, usort() in PHP.
  • Passport.js and Laravel guards: authentication strategies (password, OAuth, JWT) behind one interface.
  • Cache and retry policies in HTTP clients, and compression codecs chosen by configuration.

Key points

  • Strategy puts interchangeable algorithms behind one interface; the context uses one without knowing which.
  • It replaces algorithm-selecting if/else chains and makes each algorithm testable.
  • In Python, TypeScript, modern Java and PHP, a strategy is often just a function or lambda.
  • Further reading: Refactoring Guru: Strategy.

Practice questions

What is the Strategy pattern? Give a real-world example.

Answer

Strategy defines a family of interchangeable algorithms behind one interface; the context holds one and delegates to it, so algorithms can be swapped at runtime without if/else. Examples: shipping cost rules, payment methods, comparators passed to a sort function, authentication strategies in Passport.js.

How is the Strategy pattern usually implemented in Python or TypeScript?

Answer

Often with plain functions: pass a function (key= in sorted, a comparator in array.sort) instead of defining an interface and a class per algorithm. Use classes when a strategy needs configuration or several related methods.

How does Strategy support the Open/Closed Principle?

Answer

New behaviour is added by writing a new strategy and registering it; the context that uses strategies is never edited. The code is open for extension and closed for modification.

How do you avoid a new if/else just to choose the strategy?

Answer

Use a lookup table (a map from name to strategy), configuration, or a factory. The selection then lives in one place, often at the composition root, instead of inside business logic.

Open in the app to mark lessons complete, take notes and track your progress.