LESSON 7.1
Strategy
Define a family of algorithms, encapsulate each one, and make them interchangeable, letting the algorithm vary independently from clients that use it.
12 min read
Real-world analogy
Traveling from Manhattan to JFK Airport demonstrates the Strategy pattern. Your objective—arriving at the airport on time—remains constant. However, you can select from different transportation strategies:
- Driving your own car (cheap, but risk traffic jams)
- Taking the subway and AirTrain (fixed schedule, immune to traffic)
- Booking a helicopter transfer (fastest, but most expensive)
You choose the strategy based on current context (budget, traffic alerts, flight departure time), and you can switch strategies before leaving without changing your destination.
The problem
You are building an e-commerce checkout engine. Customers can choose different shipping providers at checkout:
StandardShipping: $5.00 flat fee + $1.50 per kilogram.ExpressShipping: $15.00 flat fee + $3.00 per kilogram.OvernightShipping: $35.00 flat fee + $7.50 per kilogram.FreeShippingPromotion: $0.00 for orders over $100.
Without the Strategy pattern, the Order or ShippingCalculator class implements all pricing formulas inside a monolithic switch or if-else statement:
// Naive approach: Monolithic conditional branching in domain context
class ShippingCalculator {
calculateCost(order, shippingType) {
let cost = 0;
if (shippingType === "STANDARD") {
cost = 5.00 + order.weightKg * 1.50;
} else if (shippingType === "EXPRESS") {
cost = 15.00 + order.weightKg * 3.00;
} else if (shippingType === "OVERNIGHT") {
cost = 35.00 + order.weightKg * 7.50;
} else if (shippingType === "FREE") {
cost = order.total > 100 ? 0 : 5.00;
} else {
throw new Error(`Unknown shipping type: ${shippingType}`);
}
return cost;
}
}public class Example {
static class Order { double weightKg; double total; }
static class ShippingCalculator {
public double calculateCost(Order order, String type) {
// Brittle: Modifying or adding a shipping carrier requires modifying
// this method, risking regressions in all other carriers.
if ("STANDARD".equals(type)) return 5.0 + order.
Every new shipping carrier (DHL, FedEx, Drone Delivery) requires modifying the calculator, violating the Open/Closed Principle. Unit testing individual pricing algorithms in isolation is impossible.
The solution
The Strategy pattern defines a family of algorithms, encapsulates each one into an independent class, and makes them interchangeable behind a common interface. The context delegates the algorithmic calculation to its current strategy object.
- Strategy (
ShippingStrategy): Interface declaring the algorithm signature (calculate(order)). - Concrete Strategies (
StandardShipping,ExpressShipping,OvernightShipping): Implements the specific algorithmic variation. - Context (
ShippingCalculator): Maintains a reference to aShippingStrategyand delegates calls to it. - Client: Configures the context with the desired strategy at runtime.
Step-by-step implementation
Step 1: Define the strategy interface
Declare the contract that all algorithm implementations must fulfill.
class ShippingStrategy {
calculateCost(weightKg, orderTotal) {
throw new Error("calculateCost() must be implemented");
}
}public class Example {
interface ShippingStrategy {
double calculateCost(double weightKg, double orderTotal);
}
Step 2: Implement concrete strategy classes
Encapsulate each calculation formula into its own dedicated class.
Step 3: Configure the context with the strategy
The context receives the strategy and delegates work to it without conditional branching.
The complete example
Here is the complete, runnable implementation demonstrating strategies as objects and registry selection maps.
Standard Shipping for 4kg: $11.00
Express Shipping for 4kg: $27.00
Overnight Shipping for 4kg: $65.00Variations
Strategies as first-class functions and lambdas
In modern languages (JavaScript, Java 8+, Python), you do not always need to declare a separate class for each strategy. Because a strategy often contains a single method, first-class functions or lambdas can be passed directly:
// Functional Strategy in JS
const standard = (weight) => 5.0 + weight * 1.5;
const express = (weight) => 15.0 + weight * 3.0;
function calculate(weight, strategyFn) {
return strategyFn(weight);
}Strategy selection maps vs switch-case
A frequent interview trap is keeping a massive switch (type) statement to instantiate strategies. This defeats the Open/Closed Principle. Instead, use a Strategy Selection Map (Map<String, ShippingStrategy>) or Dependency Injection registry. When a new carrier is introduced, simply register it in the map without modifying any control flow logic.
Where you've already used it
java.util.Comparator: Passing aComparatorlambda(a, b) -> a.compareTo(b)toCollections.sort()parameterizes the comparison strategy.- JavaScript
Array.prototype.sort(): Providing a comparator callback(a, b) => a - bconfigures the sorting strategy. - Passport.js: Authentication strategies (
LocalStrategy,JwtStrategy,OAuth2Strategy) decouple user identity verification from Express route handlers. - Spring
TaskExecutor: Strategies for executing background tasks (ThreadPoolTaskExecutor,SimpleAsyncTaskExecutor,SyncTaskExecutor).
Pros and cons
Advantages
- Open/Closed Principle: Introduce new algorithms without altering existing context classes.
- Eliminates Conditionals: Replaces complex nested
if-elseorswitchstatements with polymorphism. - Runtime Swappability: Switch algorithms dynamically during execution based on runtime conditions.
- Isolated Unit Testing: Algorithms can be unit tested in isolation without instantiating the entire context.
Disadvantages
- Client Awareness: Clients must be aware of the differences between concrete strategies to choose the appropriate one.
- Object Overhead: In object-heavy implementations, each strategy introduces a new class and heap allocation.
When not to use it
Do not use Strategy if an algorithm never changes and only has a single implementation. Hardcoding a straightforward formula directly inside the context is simpler than introducing abstract strategy interfaces.
Related patterns
| Pattern | Control Ownership | Focus |
|---|---|---|
| Strategy | Set by client externally | Swapping interchangeable algorithms |
| State | Transitioned by internal states | Changing behavior based on internal state changes |
| Command | Action to execute | Encapsulating requests as standalone objects |
Strategy: shipping rules
Refactor a shipping calculator from hardcoded switch/branch logic into the Strategy pattern, enabling registration of new shipping tiers without modifying the calculator.
Click Run tests to evaluate your solution against the behavior and structural test suite.
Interview questions
How does Strategy differ from State? In Strategy, the client explicitly injects the algorithm, and strategies do not know about each other. In State, the context's behavior changes automatically as internal state transitions occur, and states often trigger transitions to other concrete states.
How does Strategy differ from Template Method? Strategy uses object composition to delegate algorithms dynamically at runtime. Template Method uses class inheritance to define an algorithm skeleton in a base class, allowing subclasses to override specific steps at compile time.
How do you avoid large if/else or switch blocks when selecting a strategy?
Use a Strategy Registry Map (Map<Key, Strategy>) populated during application startup or via Dependency Injection. Lookups happen in $O(1)$ time without conditional statements.
Can a strategy hold state? Stateless strategies are preferred because a single shared instance can be safely reused across multiple contexts and threads. However, stateful strategies are permissible if the algorithm requires local accumulation or cache buffers.
Try it
Shopping Cart Domain LLD
Model cart mutations, version checks and reproducible price quotes.