Strategy Design Pattern


Problem

Many classes differ only in how they perform a particular task. A duck simulator needs many duck types that all swim and display, but each one flies and quacks differently. A text composer needs to break paragraphs into lines, but the linebreaking algorithm should be selectable: a fast greedy pass for an interactive editor, the TeX algorithm for high-quality typesetting, or a fixed-width strategy for icon grids. A payment system needs credit card, PayPal, and bank-transfer flows that all share the same checkout pipeline.

If you push every variant into a single class with conditional logic, the class quickly becomes unmaintainable:

class Duck {
    void fly(String type) {
        if (type.equals("mallard")) {
            // flap wings
        } else if (type.equals("rubber")) {
            // do nothing
        } else if (type.equals("decoy")) {
            // do nothing
        } else if (type.equals("rocket")) {
            // launch rockets
        }
        // every new duck adds another branch
    }
}

If you push every variant into its own subclass, you end up with deep inheritance hierarchies that fight reality: a RubberDuck inherits a fly() it must override to do nothing; a DecoyDuck inherits both fly() and quack() it must neutralize. Adding a new behavior axis (e.g., “swim with rockets”) combinatorially explodes the class hierarchy.

The core problem is: How can we vary an algorithm independently of the objects that use it, swap algorithms at runtime, and add new algorithms without touching existing client code?

Context

The Strategy pattern (also known as the Policy pattern (Gamma et al. 1995)) applies when:

  • Many related classes differ only in their behavior. Strategies provide a way to configure a class with one of many behaviors, instead of creating a subclass for each behavior (Gamma et al. 1995).
  • You need different variants of an algorithm. For example, algorithms that reflect different space/time trade-offs, or algorithms tuned for different data shapes.
  • An algorithm uses data that clients shouldn’t know about. Hiding algorithm-specific data structures behind a Strategy interface keeps clients decoupled from implementation details.
  • A class defines many behaviors that appear as multiple conditional statements. Move the conditional branches into their own Strategy classes so each branch becomes a polymorphic object (Freeman and Robson 2020).

Common applications include sorting and searching algorithms, validation rules, compression formats, payment processing flows, AI agents in games, layout/linebreaking strategies in text editors, and authentication schemes.

Solution

The Strategy pattern defines a family of algorithms, encapsulates each one as an object, and makes them interchangeable at runtime. The client (the Context) holds a reference to a Strategy interface and delegates the variable behavior to it.

The pattern involves three roles:

  1. Strategy: An interface (or abstract class) declaring the operation common to all supported algorithms. The Context uses this interface to invoke the algorithm.
  2. ConcreteStrategy: A class that implements the Strategy interface with one specific algorithm.
  3. Context: The class that uses the algorithm. It holds a reference to a Strategy object and forwards work to it. The Context typically exposes a setter so the strategy can be swapped at runtime.

The key insight is composition over inheritance: instead of locking each variant into a subclass, the Context has-a Strategy and can be re-configured at any time. This is the same insight that makes the Observer and State patterns work — replace static class hierarchies with dynamic object delegation.

UML Role Diagram

Context -strategy: Strategy +setStrategy(strategy: Strategy): void +contextInterface(): void «interface» Strategy +algorithmInterface(): void ConcreteStrategyA +algorithmInterface(): void ConcreteStrategyB +algorithmInterface(): void ConcreteStrategyC +algorithmInterface(): void strategy strategy.algorithmInterface();

Figure: the Context aggregates a Strategy and forwards work to it; ConcreteStrategies realize the interface independently. The Context never knows which concrete strategy it holds.

UML Example Diagram

The classic SimUDuck example (Freeman and Robson 2020) extracts the fly and quack behaviors out of the Duck hierarchy. Each duck has-a FlyBehavior and a QuackBehavior; the concrete strategy classes implement each variation. A MallardDuck flies with wings and quacks normally; a RubberDuck cannot fly (uses a null-object fly behavior) and squeaks instead.

«abstract» Duck -flyBehavior: FlyBehavior -quackBehavior: QuackBehavior +performFly(): void +performQuack(): void +setFlyBehavior(fb: FlyBehavior): void +display(): void MallardDuck RubberDuck «interface» FlyBehavior +fly(): void FlyWithWings FlyNullObject «interface» QuackBehavior +quack(): void Quack Squeak flyBehavior quackBehavior Null Object Strategy: do nothing on fly().

Figure: Duck delegates flying and quacking to interchangeable Strategy objects; RubberDuck swaps in FlyNullObject instead of subclassing to override.

Sequence Diagram

This sequence shows runtime reconfiguration: a ModelDuck starts with a no-op fly behavior, the client swaps in a rocket-powered strategy via setFlyBehavior, and the next performFly() call now does something completely different — without changing the Duck class.

performFly() fly() setFlyBehavior(rocket) performFly() fly() client: Client duck: ModelDuck nullFly: FlyNullObject rocket: FlyRocketPowered

Figure: the same Duck object exhibits two different fly behaviors across two performFly() calls — runtime swapping is the central capability Strategy enables.

Code Example

This example follows the SimUDuck design from Head First Design Patterns (Freeman and Robson 2020). The Duck class delegates to two strategy objects; concrete duck subclasses configure their strategies in the constructor; the client can swap a strategy at runtime by calling setFlyBehavior().

Teaching example: These snippets are intentionally small. They show one reasonable mapping of the pattern roles, not a drop-in architecture. In production, always tailor the pattern to the concrete context: lifecycle, ownership, error handling, concurrency, dependency injection, language idioms, and team conventions.

interface FlyBehavior {
    void fly();
}

interface QuackBehavior {
    void quack();
}

final class FlyWithWings implements FlyBehavior {
    public void fly() {
        System.out.println("Flapping wings");
    }
}

final class FlyNullObject implements FlyBehavior {
    public void fly() {
        // do nothing — can't fly
    }
}

final class FlyRocketPowered implements FlyBehavior {
    public void fly() {
        System.out.println("Flying with a rocket");
    }
}

final class Quack implements QuackBehavior {
    public void quack() {
        System.out.println("Quack!");
    }
}

abstract class Duck {
    protected FlyBehavior flyBehavior;
    protected QuackBehavior quackBehavior;

    void performFly() {
        flyBehavior.fly();
    }

    void performQuack() {
        quackBehavior.quack();
    }

    void setFlyBehavior(FlyBehavior fb) {
        this.flyBehavior = fb;
    }

    abstract void display();
}

final class ModelDuck extends Duck {
    ModelDuck() {
        flyBehavior = new FlyNullObject();
        quackBehavior = new Quack();
    }

    void display() {
        System.out.println("I'm a model duck");
    }
}

public class Demo {
    public static void main(String[] args) {
        Duck model = new ModelDuck();
        model.performFly();                          // does nothing
        model.setFlyBehavior(new FlyRocketPowered());
        model.performFly();                          // "Flying with a rocket"
    }
}
#include <iostream>
#include <memory>

struct FlyBehavior {
    virtual ~FlyBehavior() = default;
    virtual void fly() = 0;
};

struct QuackBehavior {
    virtual ~QuackBehavior() = default;
    virtual void quack() = 0;
};

class FlyWithWings : public FlyBehavior {
public:
    void fly() override { std::cout << "Flapping wings\n"; }
};

class FlyNullObject : public FlyBehavior {
public:
    void fly() override { /* do nothing */ }
};

class FlyRocketPowered : public FlyBehavior {
public:
    void fly() override { std::cout << "Flying with a rocket\n"; }
};

class Quack : public QuackBehavior {
public:
    void quack() override { std::cout << "Quack!\n"; }
};

class Duck {
public:
    virtual ~Duck() = default;

    void performFly() { flyBehavior_->fly(); }
    void performQuack() { quackBehavior_->quack(); }

    void setFlyBehavior(std::unique_ptr<FlyBehavior> fb) {
        flyBehavior_ = std::move(fb);
    }

    virtual void display() const = 0;

protected:
    std::unique_ptr<FlyBehavior> flyBehavior_;
    std::unique_ptr<QuackBehavior> quackBehavior_;
};

class ModelDuck : public Duck {
public:
    ModelDuck() {
        flyBehavior_ = std::make_unique<FlyNullObject>();
        quackBehavior_ = std::make_unique<Quack>();
    }

    void display() const override { std::cout << "I'm a model duck\n"; }
};

int main() {
    ModelDuck model;
    model.performFly();                                            // does nothing
    model.setFlyBehavior(std::make_unique<FlyRocketPowered>());
    model.performFly();                                            // "Flying with a rocket"
}
from abc import ABC, abstractmethod


class FlyBehavior(ABC):
    @abstractmethod
    def fly(self) -> None:
        pass


class QuackBehavior(ABC):
    @abstractmethod
    def quack(self) -> None:
        pass


class FlyWithWings(FlyBehavior):
    def fly(self) -> None:
        print("Flapping wings")


class FlyNullObject(FlyBehavior):
    def fly(self) -> None:
        pass  # do nothing — can't fly


class FlyRocketPowered(FlyBehavior):
    def fly(self) -> None:
        print("Flying with a rocket")


class Quack(QuackBehavior):
    def quack(self) -> None:
        print("Quack!")


class Duck(ABC):
    def __init__(self) -> None:
        self.fly_behavior: FlyBehavior
        self.quack_behavior: QuackBehavior

    def perform_fly(self) -> None:
        self.fly_behavior.fly()

    def perform_quack(self) -> None:
        self.quack_behavior.quack()

    def set_fly_behavior(self, fb: FlyBehavior) -> None:
        self.fly_behavior = fb

    @abstractmethod
    def display(self) -> None:
        pass


class ModelDuck(Duck):
    def __init__(self) -> None:
        super().__init__()
        self.fly_behavior = FlyNullObject()
        self.quack_behavior = Quack()

    def display(self) -> None:
        print("I'm a model duck")


model = ModelDuck()
model.perform_fly()                            # does nothing
model.set_fly_behavior(FlyRocketPowered())
model.perform_fly()                            # "Flying with a rocket"
interface FlyBehavior {
  fly(): void;
}

interface QuackBehavior {
  quack(): void;
}

class FlyWithWings implements FlyBehavior {
  fly(): void { console.log("Flapping wings"); }
}

class FlyNullObject implements FlyBehavior {
  fly(): void { /* do nothing — can't fly */ }
}

class FlyRocketPowered implements FlyBehavior {
  fly(): void { console.log("Flying with a rocket"); }
}

class Quack implements QuackBehavior {
  quack(): void { console.log("Quack!"); }
}

abstract class Duck {
  protected flyBehavior!: FlyBehavior;
  protected quackBehavior!: QuackBehavior;

  performFly(): void {
    this.flyBehavior.fly();
  }

  performQuack(): void {
    this.quackBehavior.quack();
  }

  setFlyBehavior(fb: FlyBehavior): void {
    this.flyBehavior = fb;
  }

  abstract display(): void;
}

class ModelDuck extends Duck {
  constructor() {
    super();
    this.flyBehavior = new FlyNullObject();
    this.quackBehavior = new Quack();
  }

  display(): void {
    console.log("I'm a model duck");
  }
}

const model = new ModelDuck();
model.performFly();                          // does nothing
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();                          // "Flying with a rocket"

In languages with first-class functions, a strategy is often just a functionComparator<T> in Java, Comparable in Python’s sorted(key=...), a lambda passed to Array.prototype.sort. Use an explicit Strategy class when the algorithm needs identity, configuration data, multiple operations, polymorphic dispatch beyond a single call, or test seams.

Design Decisions

How does the Strategy access Context data?

When a Strategy needs information from the Context to do its job, there are two main approaches (Gamma et al. 1995):

  • Pass data as parameters: The Context passes everything the Strategy needs through the algorithm interface (e.g., compose(componentSizes, lineWidth, breaks)). This keeps Strategy and Context decoupled, but the Context may have to pass data the Strategy doesn’t actually need.
  • Pass the Context itself: The Context passes itself as an argument, and the Strategy queries the Context for whatever data it needs (e.g., strategy.execute(this)). This lets the Strategy ask for exactly what it wants but requires Context to expose a richer interface, increasing coupling.

The right choice depends on the algorithm’s data needs and how stable the Context’s interface is.

Compile-time vs. runtime strategy selection

  • Runtime selection (the standard form): the Strategy is held as a field and can be swapped via a setter. This enables dynamic reconfiguration — exactly what setFlyBehavior() enables in the duck example.
  • Compile-time selection (C++ template parameter, generics): the Strategy is bound when the type is instantiated. This is more efficient (no virtual dispatch, possibly inlinable) but cannot change at runtime. Useful when the choice is fixed at configuration time and performance matters (Gamma et al. 1995).

Optional Strategy with default behavior

The Context can be simplified if it’s meaningful for the Strategy reference to be absent. The Context checks if a Strategy is set: if so, it delegates; if not, it falls back to a default behavior (Gamma et al. 1995). Clients that want the default never have to deal with Strategy objects at all. The Null Object variant (e.g., FlyNullObject) achieves the same effect more uniformly: a “do nothing” Strategy keeps the Context’s call site simple (flyBehavior.fly()) without null checks.

Stateless vs. stateful strategies

If a Strategy carries no instance data, it can be shared across many Contexts as a Flyweight or Singleton, saving memory and avoiding repeated allocation. If it carries per-Context configuration (e.g., a RangeValidator(min=0, max=100)), each Context needs its own Strategy instance.

Consequences

Applying the Strategy pattern yields several important consequences (Gamma et al. 1995):

  • Families of related algorithms. Strategy hierarchies define a family of interchangeable algorithms. Common functionality can be factored out via inheritance among ConcreteStrategies.
  • An alternative to subclassing. Rather than baking each algorithm variant into a Context subclass — which couples algorithm and Context tightly — Strategy encapsulates each algorithm separately. The Context becomes simpler, and algorithms can vary independently.
  • Eliminates conditional statements. Code with many if/switch branches selecting between algorithms is a strong code smell pointing to Strategy. Each branch becomes a polymorphic ConcreteStrategy. This is the polymorphism over conditions principle that also underlies the State pattern.
  • A choice of implementations. Strategies can provide different implementations of the same behavior with different time/space trade-offs (e.g., a fast approximate sort vs. a careful stable sort), letting the client choose.
  • Clients must know about the strategies. Because the client typically picks the ConcreteStrategy, it must understand how the strategies differ. If the choice should be hidden from clients, Strategy is the wrong tool.
  • Communication overhead. The Strategy interface is shared by all ConcreteStrategies. Some may not need all the data the interface passes, leading to wasted preparation in the Context.
  • Increased number of objects. Strategy adds one class per algorithm variant. Stateless strategies can be shared as flyweights to mitigate this.

Pattern Compounds and Idioms

Strategy combines naturally with other patterns:

  • Strategy + Singleton / Flyweight: Stateless strategies (e.g., Quack, Squeak) carry behavior but no data. They can be implemented as singletons or shared as flyweights to avoid creating one instance per Context.
  • Null Strategy: A “do nothing” ConcreteStrategy (e.g., FlyNullObject, MuteQuack) replaces null checks in the Context with uniform polymorphic dispatch. This is the Null Object pattern superimposed on Strategy.
  • Strategy + Factory Method / Abstract Factory: A factory selects which ConcreteStrategy to instantiate based on configuration, environment, or feature flags — keeping the Context oblivious to selection logic.
  • Strategy in MVC: In the MVC compound pattern, the Controller is a Strategy used by the View. Swapping controllers (e.g., from an editing controller to a read-only controller) reconfigures input behavior without modifying the View.

Common Examples

Domain Strategy interface Concrete strategies
Sorting Comparator<T> natural order, by-field, custom rules
Validation Validator range check, regex match, length check, composed validators
Compression Compressor gzip, zip, lz4, no-op
Payment PaymentMethod credit card, PayPal, bank transfer, gift card
Authentication AuthStrategy password, OAuth, SSO, API key
Game AI BehaviorStrategy aggressive, defensive, patrol, idle
Text layout Compositor simple greedy, TeX optimal, fixed-width array
Pricing DiscountStrategy seasonal, member, bulk, no discount

Practical Guidance: When NOT to Use Strategy

Strategy is not free. Skip it when:

  • There is only one algorithm. A single concrete class with a single method is simpler. Don’t create an interface and subclass for a variant that doesn’t exist yet — that’s speculative abstraction.
  • The variants will never change at runtime and clients don’t care. A simple inheritance hierarchy or even a parameter switch may be clearer.
  • The strategies are trivial one-liners. A function or lambda is often enough; the boilerplate of a class hierarchy is unjustified.
  • The choice is genuinely a state machine. If “which algorithm” depends on what the object is currently doing, State is the right tool — the structure looks identical but the intent differs.

As with all design patterns, keep the Rule of Three in mind: don’t introduce Strategy until you have at least three concrete variants or a clear plan for runtime swapping. The simplest code is usually the smartest design.

Flashcards

Strategy Pattern Flashcards

Key concepts, design decisions, and trade-offs of the Strategy design pattern.

What is the intent of the Strategy pattern?

What problem does Strategy solve?

What core OO principle does Strategy embody?

What are the three roles in the Strategy pattern?

How does Strategy differ from State? They have identical UML structures.

How does Strategy differ from Template Method?

What is a Null Object Strategy, and why is it useful?

Why are conditional if/switch statements selecting between algorithms a code smell that suggests Strategy?

What is the main drawback of Strategy that makes it unsuitable when the choice should be hidden from clients?

When should a Strategy be implemented as a Singleton or Flyweight?

Two ways the Context can give the Strategy access to its data — what are they, and what’s the trade-off?

Give three real-world examples of the Strategy pattern in everyday programming.

Why does the SimUDuck example put fly() and quack() into Strategy interfaces instead of using Flyable and Quackable interfaces directly on each duck?

Strategy is also known by what alternate name in the GoF catalog?

When should you NOT use Strategy?

Quiz

Strategy Pattern Quiz

Test your understanding of the Strategy pattern's structure, its composition-over-inheritance principle, and the often-confused boundary with the State pattern.

A team is designing an e-commerce checkout system. Customers can pay by credit card, PayPal, gift card, or bank transfer. The CTO wants to add support for cryptocurrency next quarter without modifying any existing checkout code. Which design best fits?

Correct Answer:

Consider this UML structure: a Context class holds a reference to an interface, and several concrete classes implement that interface. The Context delegates an operation to the held implementation, which can be swapped via a setter. Both the State and Strategy patterns have exactly this structure. What actually distinguishes them?

Correct Answer:

Which of the following are valid reasons to use the Strategy pattern? Select all that apply.

Correct Answers:

In Head First Design Patterns’ SimUDuck example, a first attempt puts fly() and quack() directly on the Duck superclass. This is then refactored to use Flyable and Quackable interfaces. Why is the interface approach still considered inferior to a Strategy-based design?

Correct Answer:

A Compositor interface defines compose(natural[], stretch[], shrink[], width, breaks[]). Three ConcreteStrategies implement it: SimpleCompositor (greedy), TeXCompositor (paragraph-optimal), and ArrayCompositor (fixed-width grids). The SimpleCompositor ignores the stretch and shrink arrays entirely. Which Strategy consequence does this illustrate?

Correct Answer:

A teammate writes:

class FlyNullObject implements FlyBehavior {
    public void fly() { /* do nothing */ }
}

Why is this preferable to leaving the flyBehavior field as null and writing if (flyBehavior != null) flyBehavior.fly(); in the Context?

Correct Answer:

Which of the following common library mechanisms is NOT a use of the Strategy pattern?

Correct Answer: