Skip to main content

Strategy Pattern

A behavioral design pattern that defines a family of interchangeable algorithms, encapsulates each in its own class, and lets clients switch between them at runtime.

What Problem Does It Solve?​

You have an OrderService that calculates shipping cost. It starts with one algorithm: standard flat-rate shipping. A week later, you add express shipping. Then overnight, then economy, then calculated-by-weight. Each new variant is added as an if/else or switch branch inside the calculateShipping() method.

The method grows to 100 lines. Tests for one algorithm accidentally affect others. Adding a new variant requires re-testing the whole method. Removing one means finding and deleting the right else if. This is the conditional complexity smell — algorithm variants buried inside one method.

The Strategy pattern solves this by extracting each algorithm into its own class behind a common interface. The client holds a reference to the interface and can swap the algorithm at runtime without knowing which one is active.

What Is It?​

The Strategy pattern has three participants:

RoleDescription
StrategyInterface that all algorithm variants implement
ConcreteStrategyA specific algorithm implementation
ContextThe class that holds a Strategy reference and delegates calculations to it

The Context doesn't know which specific strategy it holds — it just calls strategy.execute(). Swapping strategies changes behavior without changing the Context.

How It Works​

Context delegates to the Strategy interface. Swapping the concrete implementation changes the algorithm — the client and Context code don't change.

Code Examples​

Practical Demo

See Strategy Pattern Demo for runnable examples: class-based strategies, lambda strategies, Spring @Qualifier selection, and a registry approach.

Shipping Cost Calculator​

// ── Strategy interface ────────────────────────────────────────────────
@FunctionalInterface // ← Strategy with one method fits @FunctionalInterface
public interface ShippingCalculator {
BigDecimal calculate(Order order);
}

// ── Concrete Strategies ───────────────────────────────────────────────
public class FlatRateShipping implements ShippingCalculator {
public BigDecimal calculate(Order order) {
return new BigDecimal("5.99"); // ← flat fee regardless of order
}
}

public class ExpressShipping implements ShippingCalculator {
public BigDecimal calculate(Order order) {
return new BigDecimal("19.99"); // ← fixed premium rate
}
}

public class WeightBasedShipping implements ShippingCalculator {
private static final BigDecimal RATE_PER_KG = new BigDecimal("2.50");

public BigDecimal calculate(Order order) {
return RATE_PER_KG.multiply(BigDecimal.valueOf(order.getTotalWeightKg())); // ← scales with weight
}
}

// ── Context ────────────────────────────────────────────────────────────
public class OrderService {

private ShippingCalculator shippingCalculator; // ← holds the strategy

public OrderService(ShippingCalculator shippingCalculator) {
this.shippingCalculator = shippingCalculator;
}

// Allow runtime swap
public void setShippingCalculator(ShippingCalculator calc) {
this.shippingCalculator = calc;
}

public BigDecimal checkout(Order order) {
BigDecimal itemsTotal = order.getItemsTotal();
BigDecimal shipping = shippingCalculator.calculate(order); // ← delegates; doesn't know which
return itemsTotal.add(shipping);
}
}

// ── Usage ─────────────────────────────────────────────────────────────
OrderService service = new OrderService(new FlatRateShipping());
service.checkout(order1); // uses flat rate

service.setShippingCalculator(new WeightBasedShipping()); // ← runtime swap
service.checkout(order2); // now uses weight-based

Lambda Shortcut (Java 8+)​

Because ShippingCalculator is a @FunctionalInterface, you can define strategies as lambdas — no extra class needed for simple cases:

ShippingCalculator freeShipping = order -> BigDecimal.ZERO;      // ← one-liner strategy

ShippingCalculator promoRate = order ->
order.getItemsTotal().compareTo(new BigDecimal("100")) > 0
? BigDecimal.ZERO // ← free shipping over $100
: new BigDecimal("5.99");

OrderService service = new OrderService(freeShipping);
service.checkout(order);

Spring DI — Injecting Strategies​

Spring makes Strategy injection trivial — define each strategy as a @Component and inject the desired one via @Qualifier or @ConditionalOnProperty:

@Component("flatRate")
public class FlatRateShipping implements ShippingCalculator { /* ... */ }

@Component("express")
public class ExpressShipping implements ShippingCalculator { /* ... */ }

@Service
public class OrderService {
private final ShippingCalculator calculator;

// ← @ConditionalOnProperty switches strategy based on app config
public OrderService(@Qualifier("flatRate") ShippingCalculator calculator) {
this.calculator = calculator;
}
}

To pick the strategy at runtime from a config property:

@Configuration
public class ShippingConfig {
@Value("${shipping.mode:flat}")
private String mode;

@Bean
public ShippingCalculator shippingCalculator(
FlatRateShipping flat, ExpressShipping express, WeightBasedShipping weight) {
return switch (mode) {
case "express" -> express;
case "weight" -> weight;
default -> flat;
};
}
}

Comparator — Strategy in the JDK​

java.util.Comparator is the Strategy pattern built into Java. The sorting algorithm (Collections.sort, Arrays.sort) is the Context; the Comparator is the Strategy; each lambda/implementation is a ConcreteStrategy:

List<User> users = loadUsers();

// Sort by name — one strategy
users.sort(Comparator.comparing(User::getName));

// Sort by age descending — different strategy, same sorting context
users.sort(Comparator.comparing(User::getAge).reversed());

Trade-offs & When To Use / Avoid​

ProsCons
StrategyEliminates if/else branching; each algorithm is isolated and independently testable; swap at runtimeMore classes/objects if many strategies; client must be aware of which strategies exist
vs if/elseOpen/Closed — add new strategy without modifying existing code—
vs Template MethodComposition over inheritance; strategies are interchangeable objectsTemplate Method uses inheritance; preferred when the skeleton is fixed and only a step varies

When to use:

  • Multiple algorithm variants that share the same interface but differ in implementation.
  • You want to swap algorithms at runtime based on config, user preference, or request data.
  • You have a switch or long if/else chain that selects algorithm behavior.

When to avoid:

  • Only one algorithm ever needed — adds abstraction for no benefit.
  • Algorithms don't share a meaningful common interface — forced Strategy is worse than a well-placed if/else.

Common Pitfalls​

  • Stateful strategies without protection — if a ConcreteStrategy has mutable state and is shared (singleton bean), concurrent calls cause bugs. Either make strategies stateless or ensure they're prototype-scoped.
  • Context becoming a mini-factory — if the context is also responsible for choosing the strategy (if (mode == X) strategy = new X()), extract that logic into a Strategy Factory or Spring configuration.
  • Using Strategy when State is more appropriate — if the context automatically transitions between strategies based on internal state, use the State pattern instead.

Interview Questions​

Beginner​

Q: What is the Strategy pattern? A: It defines a family of algorithms, encapsulates each in its own class behind a common interface, and lets you swap them at runtime. This replaces conditional branching with polymorphism.

Q: Where does the JDK use Strategy? A: java.util.Comparator is the classic example — it's the strategy for the sorting algorithm. java.util.concurrent.RejectedExecutionHandler on ThreadPoolExecutor is another — it's the strategy for what to do when the task queue is full.

Intermediate​

Q: What is the difference between Strategy and Template Method? A: Both vary an algorithm. Template Method uses inheritance — the algorithm skeleton is in a base class, and subclasses override specific steps. Strategy uses composition — the algorithm is a separate object injected into the context. Strategy supports runtime swapping; Template Method fixes the structure at compile time.

Q: How do you implement Strategy cleanly in Spring Boot? A: Define a @FunctionalInterface as the Strategy interface. Implement each @Component variant. Inject the desired strategy via @Qualifier or pick it in a @Configuration class based on a config property. This keeps each algorithm independently testable and injectable.

Advanced​

Q: How can you support selecting a strategy dynamically at request time (not just startup)? A: Use a Strategy Registry — a Map<String, Strategy> of all registered strategies. Inject all ShippingCalculator beans (Spring supports injecting a Map<String, T> of all T beans). The Context looks up the appropriate strategy by key at request time:

@Service
public class OrderService {
private final Map<String, ShippingCalculator> calculators;

// Spring injects all ShippingCalculator beans: {"flatRate" -> ..., "express" -> ...}
public OrderService(Map<String, ShippingCalculator> calculators) {
this.calculators = calculators;
}

public BigDecimal checkout(Order order, String shippingMode) {
ShippingCalculator calc = calculators.getOrDefault(shippingMode,
calculators.get("flatRate")); // ← default fallback
return order.getItemsTotal().add(calc.calculate(order));
}
}

Further Reading​

  • Template Method Pattern — also varies an algorithm; Strategy uses composition, Template Method uses inheritance. Contrast is crucial for interviews.
  • Factory Method Pattern — often used alongside Strategy to select the right strategy object to create.
  • Observer Pattern — both are behavioral patterns; Observer decouples event producers from consumers; Strategy decouples algorithm selection from usage.