1. What Is It?
The pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from the clients that use it.
Without it, behavioral variations end up as branching logic (if/elif/switch) inside a single class. Each new variation requires modifying that class, violating Open/Closed. The class grows, tests multiply, and adding a new "" risks breaking all existing ones.
A payment processing service handles multiple payment methods (credit card, PayPal, crypto) using a long chain of if/elif blocks inside a single PaymentProcessor class. Every time a new payment method is added, the class must be modified and all existing tests re-run. Which problem does the Strategy pattern directly solve in this scenario?
2. How It Works
Participants:
- — declaring the algorithm method (e.g.,
execute(),sort(),calculate()) - ConcreteStrategy — implements a specific algorithm variant
- Context — holds a reference to a object; delegates the algorithm call to it; clients configure the Context with the desired ConcreteStrategy
Step-by-step:
- Identify the behavior that varies (e.g., sorting algorithm, pricing rule, authentication method)
- Extract that behavior into a
- Implement each variant as a
ConcreteStrategy Contextholds areference and calls it — noif/elseneeded- At runtime, inject the desired strategy into the Context
Python:
from abc import ABC, abstractmethod
from typing import List
class PricingStrategy(ABC):
@abstractmethod
def calculate(self, base_price: float) -> float:
pass
class RegularPricingStrategy(PricingStrategy):
def calculate(self, base_price: float) -> float:
return base_price
class DiscountPricingStrategy(PricingStrategy):
def __init__(self, discount_rate: float) -> None:
self._discount_rate = discount_rate
def calculate(self, base_price: float) -> float:
return base_price * (1 - self._discount_rate)
class HolidayPricingStrategy(PricingStrategy):
def calculate(self, base_price: float) -> float:
return base_price * 0.8 # 20% holiday discount
class ShoppingCart:
def __init__(self, pricing_strategy: PricingStrategy) -> None:
self._pricing_strategy = pricing_strategy
self._items: List[float] = []
def set_pricing_strategy(self, pricing_strategy: PricingStrategy) -> None:
self._pricing_strategy = pricing_strategy
def add_item(self, price: float) -> None:
self._items.append(price)
def checkout(self) -> float:
base_total = sum(self._items)
return self._pricing_strategy.calculate(base_total)
# Usage
cart = ShoppingCart(RegularPricingStrategy())
cart.add_item(100.0)
cart.add_item(50.0)
print(cart.checkout()) # 150.0
# Swap strategy at runtime — no structural change to ShoppingCart
cart.set_pricing_strategy(DiscountPricingStrategy(0.1))
print(cart.checkout()) # 135.0
cart.set_pricing_strategy(HolidayPricingStrategy())
print(cart.checkout()) # 120.0Java:
import java.util.List;
import java.util.ArrayList;
// Strategy interface
public interface PricingStrategy {
double calculate(double basePrice);
}
// ConcreteStrategy A
public class RegularPricingStrategy implements PricingStrategy {
@Override
public double calculate(double basePrice) {
return basePrice;
}
}
// ConcreteStrategy B
public class DiscountPricingStrategy implements PricingStrategy {
private final double discountRate;
public DiscountPricingStrategy(double discountRate) {
this.discountRate = discountRate;
}
@Override
public double calculate(double basePrice) {
return basePrice * (1 - discountRate);
}
}
// ConcreteStrategy C
public class HolidayPricingStrategy implements PricingStrategy {
@Override
public double calculate(double basePrice) {
return basePrice * 0.8;
}
}
// Context
public class ShoppingCart {
private PricingStrategy pricingStrategy;
private final List<Double> items = new ArrayList<>();
public ShoppingCart(PricingStrategy pricingStrategy) {
this.pricingStrategy = pricingStrategy;
}
public void setPricingStrategy(PricingStrategy pricingStrategy) {
this.pricingStrategy = pricingStrategy;
}
public void addItem(double price) {
items.add(price);
}
public double checkout() {
double baseTotal = items.stream().mapToDouble(Double::doubleValue).sum();
return pricingStrategy.calculate(baseTotal);
}
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart(new RegularPricingStrategy());
cart.addItem(100.0);
cart.addItem(50.0);
System.out.println(cart.checkout()); // 150.0
cart.setPricingStrategy(new DiscountPricingStrategy(0.1));
System.out.println(cart.checkout()); // 135.0
cart.setPricingStrategy(new HolidayPricingStrategy());
System.out.println(cart.checkout()); // 120.0
}
}Swap pricing without changing the cart
ShoppingCart checkout sums the items and delegates pricing to its strategy. The same items cost 150.0, 135.0, then 120.0 as the client swaps strategies.
The client gives the cart a regular pricing strategy.
Text version
- The client gives the cart a regular pricing strategy.
- The client adds items priced at one hundred and at fifty.
- Checkout totals the items and calls the strategy, so regular pricing returns the full amount.
- The client switches the same cart to a ten percent discount strategy.
- Ten percent off the same total of one hundred fifty gives one hundred thirty-five.
- The client switches to holiday pricing, keeping the same items.
- The holiday strategy takes twenty percent off, returning one hundred twenty.
A ShoppingCart class currently uses a chain of if/else statements to apply different pricing rules (regular, discount, holiday). You refactor it using the Strategy pattern, introducing a PricingStrategy interface and separate ConcreteStrategy classes. After the refactor, a developer needs to add a 'flash sale' pricing rule. What change is required?
3. Variants & Comparisons
| Approach | How It Works | Pros | Cons | Best For |
|---|---|---|---|---|
| Strategy (interface) | Behavior injected via interface reference | Runtime swappability, OCP-compliant | More classes | Algorithms that change frequently |
| Template Method | Algorithm skeleton in abstract class, steps overridden | Less indirection | Tied to inheritance, no runtime swap | Fixed algorithm structure with variable steps |
| if/else or switch | Branching in one method | Simple for 2–3 cases | Violates OCP, hard to extend | Truly static, rare variation |
| Lambdas/Functors | Strategy passed as function | Concise, less boilerplate | Harder to test named strategies | Simple, stateless algorithms |
In Python/Java 8+, a single-method can be replaced with a callable/lambda for simple cases. Prefer named classes when the algorithm has or complex logic.
Choose how the algorithm varies
Strategy injects behavior, Template overrides steps in a shared skeleton, and lambdas suit simple stateless algorithms.
Strategy injects an interface, Template overrides steps, and a lambda passes behavior as a function.
Text version
- Strategy injects an interface, Template overrides steps, and a lambda passes behavior as a function.
- Strategy suits frequent changes, Template suits a fixed skeleton, and lambdas suit simple stateless logic.
- Strategy adds classes, Template cannot swap at runtime, and lambdas are harder to test.
A payment processing service currently handles three payment methods using a large if/else block. The business expects to add new payment providers frequently over the coming months. Which design concern makes the if/else approach a poor long-term choice here, and what approach best addresses it?
4. When to Use It (and When NOT To)
Use when:
- You have multiple variants of an algorithm and want to swap them at runtime
- You want to eliminate a growing
if/elif/switchblock that changes whenever a new variant is added - You need to isolate algorithm logic for independent testing
Don't use when:
- You only have 2 variants and they'll never change — the costs more than it saves
- The algorithm is trivial (a one-liner) — a lambda or simple parameter is sufficient
Decision triggers:
- "Every time we add a new payment method / sort algorithm / pricing rule, I have to touch this class" →
- "I need to let users choose their algorithm at runtime" →
- "All variants share the same skeleton but differ in a couple of steps" → consider instead
Anti-patterns:
- Strategy with a single ConcreteStrategy — pointless
- Putting strategy selection logic back inside the Context (
if type == 'holiday': strategy = HolidayStrategy()) — defeats the purpose
A backend team has a checkout service that calculates shipping costs. Currently it supports two carriers, and the selection is handled by a single if/else block inside the CheckoutService class. The team expects to add three more carriers over the next quarter, each requiring different calculation logic. Which of the following best justifies applying the Strategy pattern here?
5. Real-World Usage
1. Java's Comparator : Collections.sort(list, comparator) accepts any Comparator<T> — a classic . You swap the comparison algorithm without touching the sort code. Comparator.naturalOrder(), Comparator.reverseOrder(), and custom comparators are all ConcreteStrategies.
2. Spring Framework AuthenticationProvider: Spring Security's authentication pipeline uses — AuthenticationProvider is the , and DaoAuthenticationProvider, JwtAuthenticationProvider, etc., are ConcreteStrategies. The AuthenticationManager (Context) delegates without knowing which provider is active.
3. Python's sort() / sorted() key parameter: sorted(items, key=lambda x: x.price) uses a callable strategy for comparison. The sorting algorithm is fixed; the comparison logic varies — exactly the Strategy intent.
You are building a product listing API that needs to return items sorted by different criteria depending on the request — price, rating, or name. You want to add new sort criteria in the future without modifying the sorting pipeline itself. Which design approach best fits this requirement, and why?
6. Interview Cheat Sheet
Key sentences:
- " replaces conditional branching with — instead of asking 'which algorithm?' at runtime with an if/else, you inject the right implementation up front."
- "The Context is closed for modification but open for extension — adding a new requires zero changes to the Context."
- "Strategy and both handle algorithm variation, but Strategy uses and allows runtime swapping; uses and is fixed at compile time."
- "The pattern shines when the number of variants is expected to grow — each new variant is a new class, not a modified method."
Common follow-up questions:
Q: How is Strategy different from ? A: Both delegate to an encapsulated object, but transitions happen internally based on object state; Strategy is injected externally by the client. Context drives Strategy changes; object's own logic drives State transitions.
Q: Can you use lambdas instead of Strategy classes?
A: Yes, for simple stateless algorithms. When the algorithm needs configuration (e.g., DiscountPricingStrategy(0.1)) or multiple methods, a named class is cleaner and more testable.
Q: How do you decide between Strategy and a simple map of functions? A: A map works for pure dispatch. Strategy is better when strategies have state, need injection/mocking, or when you want type safety guaranteeing the right .
Connections:
- Strategy + → creates the right ConcreteStrategy based on context
- Strategy + → Context depends on Strategy , not concrete implementations
- Strategy vs. Template Method → vs. trade-off for algorithm variation