🧩 Section 2 · Question #10

When to use Strategy

The Strategy pattern is a Behavioral design pattern that defines a family of interchangeable algorithms, encapsulates each one into a dedicated class, and makes them swappable a...


🟢 Junior Level

The Strategy pattern is a Behavioral design pattern that defines a family of interchangeable algorithms, encapsulates each one into a dedicated class, and makes them swappable at runtime without altering client code.


Real-World Analogy

Navigating to the airport: You can reach the destination via taxi, express train, or carsharing. The end objective is identical (getting to the airport), but the algorithm, time, and cost vary. You select the appropriate “strategy” dynamically right before departing based on your budget, schedule, and traffic conditions.


Basic Implementation

import java.math.BigDecimal;

// 1. Strategy Interface
public interface PaymentStrategy {
    void pay(BigDecimal amount);
}

// 2. Concrete Strategies
public class CardPaymentStrategy implements PaymentStrategy {
    public void pay(BigDecimal amount) {
        System.out.println("Processing card payment: $" + amount);
    }
}

public class CryptoPaymentStrategy implements PaymentStrategy {
    public void pay(BigDecimal amount) {
        System.out.println("Processing crypto payment: $" + amount);
    }
}

// 3. Context
public class CheckoutService {
    private PaymentStrategy strategy;

    public void setStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void completeOrder(BigDecimal total) {
        if (strategy == null) {
            throw new IllegalStateException("Payment strategy has not been selected!");
        }
        strategy.pay(total);
    }
}

When to Use Strategy

  1. Multiple Behavioral Variants: A class needs to perform a specific task using different algorithms that can be chosen at runtime.
  2. Eliminating Complex Conditionals: Code is cluttered with sprawling if-else or switch statements that select behavior based on type codes or flags.
  3. Isolating Business Logic: Algorithm details, auxiliary dependencies, and mathematical calculations should be decoupled from the core business domain.

When NOT to Use Strategy

  • When an operation only has one or two trivial conditions that are static and unlikely to change (a simple ternary operator or if-else is vastly cleaner).
  • When algorithms share internal mutable state that cannot be cleanly decoupled.

🟡 Middle Level

Eliminating the switch Anti-Pattern & Adhering to OCP

In procedural Java code, selecting an algorithm is frequently written using a switch statement:

// ❌ ANTI-PATTERN: Violates Open/Closed Principle (OCP)
public class DeliveryPriceCalculator {
    public BigDecimal calculateCost(DeliveryType type, Parcel parcel) {
        switch (type) {
            case COURIER:
                return parcel.getWeight().multiply(BigDecimal.valueOf(15.0));
            case POST:
                return parcel.getWeight().multiply(BigDecimal.valueOf(5.0));
            case PICKUP_POINT:
                return BigDecimal.valueOf(10.0);
            default:
                throw new IllegalArgumentException("Unsupported delivery type: " + type);
        }
    }
}

Every time a new delivery method (e.g., DRONE) is introduced, the existing calculator must be modified and re-tested. The Strategy pattern moves each case into a self-contained class, adhering strictly to the Open/Closed Principle: new algorithms are added by introducing new classes without modifying existing code.


The Enum Strategy Pattern

For a fixed, finite set of algorithms, an enum with an abstract method provides a compact, type-safe implementation:

public enum DiscountStrategy {
    NO_DISCOUNT {
        @Override
        public BigDecimal apply(BigDecimal price) {
            return price;
        }
    },
    VIP_CLIENT {
        @Override
        public BigDecimal apply(BigDecimal price) {
            return price.multiply(BigDecimal.valueOf(0.85)); // 15% discount
        }
    },
    PROMO_CODE {
        @Override
        public BigDecimal apply(BigDecimal price) {
            return price.multiply(BigDecimal.valueOf(0.90)); // 10% discount
        }
    };

    public abstract BigDecimal apply(BigDecimal price);
}

Modern Java: Functional Interfaces & Lambdas

In Java 8+, you don’t need to declare verbose class hierarchies for simple algorithms. Standard functional interfaces (Function<T, R>, UnaryOperator<T>, Predicate<T>) serve as lightweight strategies:

public class TextFormatter {
    public static String format(String input, UnaryOperator<String> strategy) {
        return strategy.apply(input);
    }
}

// Client usage via method references and lambdas:
String upper = TextFormatter.format("hello", String::toUpperCase);
String trimmed = TextFormatter.format("  hello  ", String::strip);
String kebab = TextFormatter.format("hello world", s -> s.replace(" ", "-"));

Spring Framework: Auto-Registry Strategy Pattern

In enterprise Spring Boot applications, the Strategy pattern is typically implemented via automatic dependency injection into a Map or List:

public interface NotificationSender {
    NotificationChannel getChannel();
    void send(String recipient, String message);
}

@Component
public class TelegramNotificationSender implements NotificationSender {
    public NotificationChannel getChannel() { return NotificationChannel.TELEGRAM; }
    public void send(String to, String msg) { /* Telegram API call */ }
}

@Component
public class EmailNotificationSender implements NotificationSender {
    public NotificationChannel getChannel() { return NotificationChannel.EMAIL; }
    public void send(String to, String msg) { /* SMTP API call */ }
}

@Service
public class NotificationRoutingService {
    private final Map<NotificationChannel, NotificationSender> senderRegistry;

    // Spring automatically discovers and injects ALL beans implementing NotificationSender!
    public NotificationRoutingService(List<NotificationSender> senders) {
        this.senderRegistry = senders.stream()
            .collect(Collectors.toMap(NotificationSender::getChannel, Function.identity()));
    }

    public void routeNotification(NotificationChannel channel, String to, String msg) {
        NotificationSender sender = senderRegistry.get(channel);
        if (sender == null) {
            throw new UnsupportedOperationException("Channel not supported: " + channel);
        }
        sender.send(to, msg);
    }
}

🔴 Senior Level

JIT Compilation & Polymorphic Call Sites

When an algorithm is invoked via an interface (strategy.execute()), the HotSpot JVM JIT compiler (C2) monitors invocation profiling data (Inline Cache):

HotSpot JIT Call Site Polymorphism:
┌─────────────────────┐ ──> Monomorphic (1 class)  ──> Speculative Inlining (0 ns overhead)
│ strategy.execute()  │ ──> Bimorphic   (2 classes)──> Inlined with conditional branch check
└─────────────────────┘ ──> Megamorphic (>2 classes)──> vtable/itable dispatch (~5–10 ns overhead)
  1. Monomorphic Call Site (1 concrete implementation): The JIT compiler speculatively devirtualizes the call and inlines the method body directly into the caller. The dispatch overhead drops to 0 ns.
  2. Bimorphic Call Site (2 concrete implementations): The JIT compiler emits an optimized type check (if (class == CardPaymentStrategy)) and inlines both target branches.
  3. Megamorphic Call Site (>2 implementations): The JIT compiler abandons inlining. The invocation is dispatched through the virtual method table (vtable) or interface table (itable), introducing indirect memory loads and CPU instruction cache line penalties.

⚡ Hot-Path Optimization (HFT / High-RPS): In performance-critical tight loops, if dozens of strategies are invoked through the same interface reference, a switch on an enum compiling to bytecode tableswitch (an $O(1)$ direct jump table) can outperform megamorphic interface dispatch.


Architectural Comparison: Strategy vs State vs Template Method

Dimension Strategy State Template Method
Mechanism Composition: Context delegates to an injected strategy. Composition: Context delegates to its current state object. Inheritance: Subclass overrides specific hook methods.
Transition Controller Client: Outside caller injects the desired strategy into the context. Context / State: State transitions occur internally during lifecycle events. Fixed Framework: Base class controls execution flow.
Statefulness Typically Stateless (thread-safe, reusable). Typically Stateful (tracks entity progress). Inherited State within class hierarchy.
Scope of Change Swaps the entire algorithm wholesale. Changes behavior across multiple operations based on state. Overrides specific steps of a fixed workflow.

Context Object Pattern for Clean Signatures

When different strategies require varying inputs, passing multiple parameters breaks interface cohesion:

// ❌ Poor design: Parameter bloat where some arguments are unused by certain strategies
void execute(String id, BigDecimal amount, String currency, String user, String ipAddress);

The Solution: Encapsulate input parameters into an immutable Java Record Context Object:

public record ExecutionContext(
    String orderId,
    BigDecimal amount,
    String currency,
    UserInfo user,
    Map<String, Object> metadata
) {}

public interface ExecutionStrategy {
    ExecutionResult process(ExecutionContext ctx);
}

4 Tricky Questions

1. How does the Strategy pattern fundamentally differ from the State pattern in architectural intent and state transition ownership?

Answer: While both patterns share an almost identical UML structural diagram based on composition and delegation, their intent and transition ownership are opposite:

  • Strategy: The caller (client) explicitly selects and injects the algorithm from the outside (e.g., choosing a payment method at checkout). The strategies are usually stateless and have no awareness of other strategies.
  • State: The context object or the state classes themselves transition between states internally as events occur in the object’s lifecycle (e.g., Order transitioning from Draft $\to$ Paid $\to$ Shipped). Concrete states are closely aware of each other and orchestrate valid transitions.

2. How can you implement the Strategy pattern in Spring Boot without writing manual factory or switch classes?

Answer: By leveraging Spring’s auto-collection injection. Declare an interface for the strategy with an identifying discriminator method (e.g., getType()). Annotate all concrete strategy implementations as @Component. In the consumer service, inject List<MyStrategy> or Map<String, MyStrategy> via constructor injection. During initialization, transform the list into a Map<Type, MyStrategy> using Collectors.toMap(). When requests arrive, perform an $O(1)$ lookup in the map by discriminator, achieving dynamic dispatch without manual factory code.


3. When does applying the Strategy pattern become an anti-pattern (Overengineering)?

Answer: Strategy becomes an anti-pattern when applied to simple, static branching logic that has only two trivial outcomes and is unlikely to grow—such as toggling an order direction with boolean isAscending. Creating an interface, two concrete classes, and a context object for a 3-line calculation introduces unnecessary abstraction and cognitive load. In modern Java, standard functional lambdas (Comparator.naturalOrder()) or ternary expressions are vastly preferred.


4. What is a megamorphic call site in the context of the Strategy pattern, and how does it impact JVM JIT performance?

Answer: A call site is megamorphic when the JIT compiler observes more than two different concrete implementation classes invoked through the same interface method signature (e.g., strategy.calculate()). When this threshold is crossed, the HotSpot C2 compiler disables speculative method inlining and inline caching. The invocation falls back to an indirect interface table (itable) dispatch, incurring indirect memory loads, instruction pipeline stalls, and branch mispredictions.


🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“The Strategy pattern encapsulates a family of interchangeable algorithms behind a common interface, allowing them to be swapped dynamically at runtime without modifying client code. It eliminates monolithic switch/if-else blocks and adheres strictly to the Open/Closed Principle. In modern enterprise Java, simple strategies are implemented via lambdas or enum strategies, while in Spring Boot applications, strategies are marked as @Component and auto-wired into a Map registry via constructor injection.”


Key Architectural Takeaways

  • Open/Closed Principle: Add new behaviors by introducing new strategy classes without touching existing business code.
  • Spring Auto-Registry: Inject List<Strategy> into the constructor to automatically populate a Map<Key, Strategy>.
  • Modern Java: Use lambdas (Function<T, R>), method references, or enum strategy definitions to reduce boilerplate.
  • Context Object: Pass a Java Record Context to prevent method signature bloat.
  • Performance Trade-Off: Monomorphic/bimorphic calls are inlined by the JIT (0 ns); megamorphic calls (>2 implementations) incur indirect virtual table dispatch (~5–10 ns).

Red Flags & Anti-Patterns

  • 🚩 Creating bloated classes for one-line operations: Use standard lambdas (Function, Predicate) instead.
  • 🚩 Storing mutable state inside strategy instances: Strategies must remain stateless to ensure multithreaded safety.
  • 🚩 Polluting method signatures with excessive parameters: Encapsulate inputs into a Context record.
  • 🚩 Instantiating strategies with new in Spring applications: Use Spring dependency injection.