StudyDSA logoStudyDSA

Command Palette

Search for a command to run...

Sign InSign Up
Sign Up

Data StructuresAlgorithmsBig-O NotationDesign PatternsSystem DesignMachine LearningPhysicsRoboticsAI Research

Factory MethodAbstract FactoryBuilderPrototypeSingletonAdapterBridgeCompositeDecoratorFacadeFlyweightProxyChain of ResponsibilityCommandIteratorMediatorMementoObserverStateStrategyTemplate MethodVisitor

Definition
StudyDSA

Where complexity meets clarity.
By Armas Zarra.

Topics

  • Data Structures
  • Algorithms
  • Big-O Notation
  • Robotics
  • AI Research
  • Machine Learning

Practice

  • Blind 75
  • LeetCode 75
  • NeetCode 150

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Armas Films LLC

Design PatternsStrategy

Strategy

A behavioral pattern that hands an algorithm in as a swappable value, so the code that runs it never changes when the family of algorithms grows.

Definition

Somewhere in checkout, a conditional picks between three pricing formulas, and the formulas, the picking, and the code that uses the answer all live tangled in one function. Strategy is the Gang of Four behavioral pattern whose stated intent is to define a family of algorithms, encapsulate each one, and make them interchangeable, letting the algorithm vary independently from the clients that use it.


A strategy is really just an algorithm handed in as a value. The context runs whatever it's holding, the client decides what it holds, and the formulas become named, swappable, individually testable things instead of branches elbowing each other inside a function that grows with every business deal.

Three Formulas in a Trench Coat

Here's the conditional version of a shipping calculator: flat rate, by weight, and express, three unrelated formulas sharing a function and a mode flag that every caller has to thread through. For an 8 kilogram package the three answers are worth computing by hand before checking the comments:

They print 10, 16, and 23. The function works, and its growth pattern is the problem: every new carrier deal lands as another branch in the same crowded body, testing any one formula means routing through the flag, and the string "express" travels through every layer of the call stack as an unchecked contract. The formulas have no names, no homes, and no independence.

Swap the Algorithm, Keep the Caller

The strategy version gives each formula a class honoring one signature, and a Checkout context that runs whichever it holds. The client constructs with flat rate, then swaps to by-weight, then express, calling the identical total(8) each time. Predict the three outputs and notice which class never changes:

The same 10, 16, 23, from one unchanged total() running three different algorithms. Each formula now has a name, its own file if it wants one, and a one-line unit test. A new carrier deal is a new class that touches nothing existing, and the comment in set_strategy() flags the detail that will matter shortly: the client makes every swap, never the strategies themselves.

Who Does What

Three roles. The Strategy interface (ShippingStrategy) fixes the signature the whole family shares. ConcreteStrategies implement one algorithm each, independent of and ignorant about their siblings. The Context (Checkout) holds one strategy and delegates, and the Client, the role that's easy to forget, picks which strategy the context holds and when that changes.


The pattern works like the travel-mode buttons on a maps app: driving, walking, and transit are three routing algorithms behind one "get directions" button, and the map renders whichever engine you picked without the engines ever switching themselves. The analogy carries the pattern's honest fine print, because somebody still does the picking. In the app it's your thumb, and in code it's selection logic that lives somewhere, often a small factory mapping mode names to strategies. The conditional doesn't vanish, it moves to one home and out of every algorithm user's way, and that relocation is the actual win.

Strategy: Step-by-Step

Installing the pattern is four moves:

1. Carve the Formula Out of the Flow

Each branch of the old conditional becomes its own unit, FlatRate, ByWeight, Express, holding one formula and nothing else. Small, named, and testable in isolation, which the branches never were.

2. Make Every Variant Honor One Signature

All strategies answer the same question the same way: cost(weight_kg) in, integer out. The shared signature is what makes them interchangeable, and anything a variant needs beyond it travels in that variant's own constructor.

3. Hand the Algorithm In From Outside

Checkout receives its strategy and runs whatever it holds, with total() reduced to one delegation. The context computes nothing itself, which is why it never changes when the algorithm roster does.

4. Centralize the Choosing

Something still decides flat versus express, and that selection logic deserves exactly one home, a factory or a config mapping. The pattern removes the conditional from the algorithm's users, not from the universe.
The Identical Twin With Different Hands

You might think this page and State describe one pattern, because the class diagrams are the same picture: a context holding an interface, concrete classes behind it, a setter for swapping. Whose hand is on the setter is the entire difference. Here, the client called set_strategy() all three times while the strategies stayed oblivious to each other's existence. In State, the state objects call the setter themselves, appointing their own successors, and the client just feeds events to a machine that steers itself.


The grep test from the State page works in both directions: find the setter's callers, and client-side calls mean Strategy while calls from inside the concrete classes mean State. The other cousin worth a sentence is Template Method, which also varies an algorithm but does it through inheritance at compile time, subclasses overriding steps of a fixed skeleton, where Strategy swaps whole algorithms through composition at runtime.

The Costume Comes Off

Strategy is the pattern that first-class functions partially dissolved, a point API designers have been making since lambdas reached Java: a one-method interface is a function type in ceremonial dress. The canonical sighting makes the case by itself. java.util.Comparator is a strategy for ordering, Collections.sort(list, comparator) is a context accepting one, and since lambdas arrived nobody writes a Comparator class when (a, b) -> a.age - b.age does it inline.


The Go and Rust tabs take the costume off completely. Go's strategy is a named function type, func(int) int, with plain functions as the variants, the same shape sort.Slice uses for its comparison argument. Rust stores a Box<dyn Fn> and takes closures, the shape of every sort_by call you've written. The class diagram survives where strategies carry configuration or several methods, and everywhere else the pattern lives on as its own ghost: a function parameter.

Best Practices

The pattern is an interface and a field, and these five keep the family honest:


A pure weight-in, cost-out strategy can be shared, swapped, and tested with a one-line assertion. Strategies that accumulate state stop being interchangeable values and start being objects with lifecycles, which is a different and heavier design.

Check Yourself

Two questions before you go. First: the checkout printed 10, 16, and 23 from one unchanged method. Who made each algorithm choice, at what moment, and what would it mean for the pattern's identity if the strategies had made it instead? Second: the mode conditional didn't disappear from the program. Where did it go, and why is that still a win? Both answers are on this page.


Then go spot it in the wild, where this pattern hides inside arguments. Every comparator you pass to a sort, every retry policy handed to an HTTP client, and every pricing rule a checkout engine loads by plan tier is an algorithm injected from outside. Last, find a function in your own codebase that takes a mode flag and switches on it. Each branch is a strategy whose name the flag is hiding, and the refactor on this page is the act of letting them introduce themselves.

the_mode_flag.py

def calculate_shipping(mode: str, weight_kg: int) -> int:
    # Three unrelated formulas sharing one function, and every
    # new carrier deal lands as another elif right here
    if mode == "flat":
        return 10
    elif mode == "weight":
        return 2 * weight_kg
    elif mode == "express":
        return 15 + weight_kg
    raise ValueError("unknown mode: " + mode)

def checkout_total(mode: str, weight_kg: int) -> int:
    # The caller threads the mode flag through, knowing too much
    return calculate_shipping(mode, weight_kg)

# Example usage
print(checkout_total("flat", 8))     # 10
print(checkout_total("weight", 8))   # 16
print(checkout_total("express", 8))  # 23

strategy.py

from abc import ABC, abstractmethod

class ShippingStrategy(ABC):  # Strategy: one algorithm family, one signature
    @abstractmethod
    def cost(self, weight_kg: int) -> int: ...

class FlatRate(ShippingStrategy):  # ConcreteStrategy
    def cost(self, weight_kg: int) -> int:
        return 10

class ByWeight(ShippingStrategy):  # ConcreteStrategy
    def cost(self, weight_kg: int) -> int:
        return 2 * weight_kg

class Express(ShippingStrategy):  # ConcreteStrategy
    def cost(self, weight_kg: int) -> int:
        return 15 + weight_kg

class Checkout:  # Context: runs whatever algorithm it's holding
    def __init__(self, strategy: ShippingStrategy):
        self._strategy = strategy

    def set_strategy(self, strategy: ShippingStrategy) -> None:
        self._strategy = strategy  # the CLIENT calls this, never the strategies

    def total(self, weight_kg: int) -> int:
        return self._strategy.cost(weight_kg)

# Example usage: the client swaps algorithms, total() never changes
checkout = Checkout(FlatRate())
print(checkout.total(8))  # 10

checkout.set_strategy(ByWeight())
print(checkout.total(8))  # 16

checkout.set_strategy(Express())
print(checkout.total(8))  # 23