6 min read
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Strategy Pattern
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.0
Java:
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 } }
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.
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
Glossary History
Click dotted jargon to save explanations here.
Glossary History
Click dotted jargon to save explanations here.