Intermediate 12 min · March 06, 2026

Strategy Pattern: Switch in Context Caused 47-Min Outage

A switch statement in the Strategy Pattern's Context blocked Apple Pay for 47 minutes.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • The Strategy Pattern extracts interchangeable algorithms into separate classes behind a shared interface
  • Context delegates to a strategy interface – never a concrete implementation
  • Adding a new algorithm means writing one class – zero changes to existing code
  • Java 8 lambdas eliminate boilerplate for single-method strategies
  • Most common failure: selecting the strategy inside the Context (defeats the pattern)
  • Performance cost: negligible – one vtable dispatch per method call
Plain-English First

Imagine you're navigating to a coffee shop. You can walk, bike, or take the bus — the destination is the same, but the method of getting there changes. Your phone's maps app doesn't rewrite itself for each transport type; it just swaps in a different 'travel strategy' at runtime. That's exactly what the Strategy Pattern does in code: it lets you swap out the algorithm (the how) without touching the logic that uses it (the what).

Every real application eventually hits this wall: a class that needs to do something slightly differently depending on the situation. Maybe it's a payment system that needs to handle credit cards, PayPal, and crypto. Maybe it's a sorting engine that chooses between quicksort and mergesort based on data size. The naive fix is a chain of if-else or switch statements inside the class itself. That works — until the requirements change, which they always do. Now you're cracking open a class that was already tested and trusted, and every edit is a potential regression.

The Strategy Pattern is a behavioural design pattern from the Gang of Four that fixes exactly this problem. Instead of stuffing multiple algorithms into one bloated class, you extract each algorithm into its own small, focused class behind a common interface. The original class just holds a reference to whichever strategy it currently needs and delegates the work to it. Adding a new algorithm means writing a new class — nothing already working gets touched.

By the end of this article you'll understand why the Strategy Pattern exists at a design level, how to implement it cleanly in Java with a realistic payment-processing example, how it compares to just using plain inheritance, and the two mistakes that trip up almost every developer the first time they reach for this pattern. You'll also walk away with sharp answers for the interview questions that always come up around it.

The Problem Strategy Solves — Why if-else Doesn't Scale

Let's build the problem before we solve it. You're writing a checkout system. Initially you only support credit cards, so you write the charge logic right inside your OrderProcessor class. Three months later, PayPal lands on the roadmap. You add an if block. Then crypto arrives. Then buy-now-pay-later. Before long your processPayment method is 150 lines of conditional logic, and every new payment type requires a developer to read, understand, and carefully not break the existing branches.

This violates the Open/Closed Principle — one of the SOLID principles — which says a class should be open for extension but closed for modification. Every time you add a payment method, you're modifying OrderProcessor. That class is now fragile: a bug introduced for crypto can accidentally break credit card processing.

The pain is real: testing becomes harder because you can't test each algorithm in isolation, the class grows without bound, and onboarding a new developer means handing them a wall of conditionals with no clear seams. The Strategy Pattern gives you those seams.

NaiveOrderProcessor.javaJAVA
1
2
3
4
5
6
7
8
9
10
// ❌ THE ANTI-PATTERN — what NOT to do
// Every new payment method forces us to edit this class.
// One typo here can break ALL payment methods at once.

public class NaiveOrderProcessor {

    // The payment type is passed as a raw string — brittle and error-prone
    public void processPayment(String paymentType, double amount) {\n\n        if (paymentType.equals(\"CREDIT_CARD\")) {\n            // Credit card processing logic\n            System.out.println(\"Charging $\" + amount + \" to credit card via Stripe API\");\n            System.out.println(\"Applying 1.5% transaction fee\");\n\n        } else if (paymentType.equals(\"PAYPAL\")) {\n            // PayPal processing logic — completely different flow\n            System.out.println(\"Redirecting to PayPal gateway for $\" + amount);\n            System.out.println(\"Applying 2.9% PayPal fee\");\n\n        } else if (paymentType.equals(\"CRYPTO\")) {\n            // Crypto logic — yet another completely different flow\n            System.out.println(\"Opening crypto wallet for $\" + amount + \" in BTC\");\n            System.out.println(\"Waiting for blockchain confirmation...\");\n\n        } else {\n            throw new IllegalArgumentException(\"Unknown payment type: \" + paymentType);\n        }\n    }\n\n    public static void main(String[] args) {\n        NaiveOrderProcessor processor = new NaiveOrderProcessor();\n        processor.processPayment(\"CREDIT_CARD\", 99.99);\n        processor.processPayment(\"PAYPAL\", 49.50);\n        // Adding a new payment type means coming back HERE and adding another else-if\n    }\n}",
        "output": "Charging $99.99 to credit card via Stripe API\nApplying 1.5% transaction fee\nRedirecting to PayPal gateway for $49.5\nApplying 2.9% PayPal fee"
      }

UML Class Diagram: The Four-Component Structure

The Strategy Pattern consists of four essential components: the Context, the Strategy interface, and one or more Concrete Strategies. The UML diagram below shows how they interact. The Context holds a reference to the Strategy interface and delegates algorithm execution to it. Concrete Strategies implement the interface, each providing a different variant of the algorithm. The Client creates a ConcreteStrategy instance and passes it to the Context, typically via constructor injection.

strategy-uml.txtTEXT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────────────────────┐
│                         UML Diagram: Strategy Pattern            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌─────────────┐      ┌──────────────────────────┐            │
│   │   Context    │      │    <<interface>>         │            │
│   │             │      │     Strategy             │            │
│   │ - strategy   │──────▶│ + execute()             │            │
│   │ + execute()  │      └──────────────────────────┘            │
│   └─────────────┘                ▲                              │
│                                  │ implements                   │
│          ┌───────────────────────┼───────────────┐              │
│          │                       │               │              │
│ ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐│
│ │ ConcreteStrategyA│  │ ConcreteStrategyB│  │ ConcreteStrategyC││
│ │ + execute()     │  │ + execute()     │  │ + execute()     ││
│ └──────────────────┘  └──────────────────┘  └──────────────────┘│
│                                                                 │
│   Client creates strategy and injects into Context              │
└─────────────────────────────────────────────────────────────────┘
Key Insight:
The Context never knows which ConcreteStrategy it is using. It only depends on the Strategy interface. This loose coupling is what makes the pattern so flexible.
Production Insight
UML diagrams are great for communicating design intent, but in production, rely on interface contracts and dependency injection to enforce the separation. The diagram is a map, not the territory.
Key Takeaway
The four components — Context, Strategy interface, Concrete Strategies, Client — form the backbone of the pattern. Understanding their relationships is essential for correct implementation.

Before/After: How the Strategy Pattern Eliminates Conditional Logic

Let's see the tangible difference between a hard-coded if-else approach and the Strategy Pattern. Consider a system that must sort data differently based on its size: small datasets use insertion sort for speed, large datasets use merge sort for stability. The naive approach puts the selection logic inside the sorting class. The Strategy Pattern extracts each sorting algorithm into its own class, making the system open for extension.

BeforeAfterSorting.javaJAVA
1
2
3
4
5
6
7
// ── BEFORE: Hard-coded if-else ──────────────────────────────
// This class must be modified every time a new algorithm is added.
class SortManager {

    public void sort(int[] data, String algorithm) {\n        if (algorithm.equals(\"quicksort\")) {\n            // quicksort logic...\n            System.out.println(\"Sorting with quicksort\");\n        } else if (algorithm.equals(\"mergesort\")) {\n            // mergesort logic...\n            System.out.println(\"Sorting with mergesort\");\n        } else {\n            throw new IllegalArgumentException(\"Unknown algorithm\");\n        }\n    }\n}\n\n// ── AFTER: Strategy Pattern ───────────────────────────────────\ninterface SortStrategy {\n    void sort(int[] data);\n}\n\nclass QuickSortStrategy implements SortStrategy {\n    public void sort(int[] data) {\n        System.out.println(\"Sorting with quicksort\");\n    }\n}\n\nclass MergeSortStrategy implements SortStrategy {\n    public void sort(int[] data) {\n        System.out.println(\"Sorting with mergesort\");\n    }\n}\n\nclass SortContext {\n    private SortStrategy strategy;\n\n    public SortContext(SortStrategy strategy) {\n        this.strategy = strategy;\n    }\n\n    public void executeSort(int[] data) {\n        strategy.sort(data);\n    }\n}\n\n// Client code selects the strategy externally\nSortContext context = new SortContext(new QuickSortStrategy());\ncontext.executeSort(data); // No if-else inside SortContext!",
        "output": "Sorting with quicksort"
      }

Building the Strategy Pattern from Scratch — A Clean Payment System

The Strategy Pattern has three moving parts: the Strategy interface (the contract every algorithm must honour), the Concrete Strategies (the actual algorithm implementations), and the Context (the class that uses a strategy without caring which one it is).

The Context holds a reference to a PaymentStrategy interface — not to any specific implementation. This is the critical move. Because the Context talks to the interface, you can hand it any concrete strategy at runtime and it just works. This is dependency injection in its simplest, most elegant form.

Notice what you gain immediately: each payment class is small, focused, and independently testable. You can write a unit test for CryptoPaymentStrategy without ever touching OrderProcessor. Adding a new payment method means writing one new class and wiring it in — zero modifications to existing, tested code. That's the Open/Closed Principle in action.

StrategyPatternPayment.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// ✅ THE STRATEGY PATTERN — clean, extensible, and testable

// ── STEP 1: Define the Strategy interface ──────────────────────────────────
// Every payment method MUST implement this contract.
// The Context (OrderProcessor) only ever talks to this interface.
interface PaymentStrategy {
    void pay(double amount);          // the algorithm hook
    double calculateFee(double amount); // bonus: fees are also encapsulated per strategy
}

// ── STEP 2: Concrete Strategy A — Credit Card ──────────────────────────────
class CreditCardPaymentStrategy implements PaymentStrategy {

    private final String cardHolderName;
    private final String maskedCardNumber; // store only last 4 digits for safety

    public CreditCardPaymentStrategy(String cardHolderName, String maskedCardNumber) {\n        this.cardHolderName = cardHolderName;\n        this.maskedCardNumber = maskedCardNumber;\n    }

    @Override
    public void pay(double amount) {
        double fee = calculateFee(amount);
        double totalCharged = amount + fee;
        // This class ONLY knows about credit card logic — nothing else
        System.out.printf("[Credit Card] Charging $%.2f (+ $%.2f fee = $%.2f total) "
                + "to card ending in %s for %s%n",
                amount, fee, totalCharged, maskedCardNumber, cardHolderName);
    }

    @Override
    public double calculateFee(double amount) {
        return amount * 0.015; // Stripe charges 1.5%
    }
}

// ── STEP 3: Concrete Strategy B — PayPal ───────────────────────────────────
class PayPalPaymentStrategy implements PaymentStrategy {

    private final String paypalEmail;

    public PayPalPaymentStrategy(String paypalEmail) {
        this.paypalEmail = paypalEmail;
    }

    @Override
    public void pay(double amount) {
        double fee = calculateFee(amount);
        double totalCharged = amount + fee;
        System.out.printf("[PayPal] Redirecting %s to PayPal for $%.2f "
                + "(+ $%.2f PayPal fee = $%.2f total)%n",
                paypalEmail, amount, fee, totalCharged);
    }

    @Override
    public double calculateFee(double amount) {
        return amount * 0.029; // PayPal charges 2.9%
    }
}

// ── STEP 4: Concrete Strategy C — Crypto ───────────────────────────────────
class CryptoPaymentStrategy implements PaymentStrategy {

    private final String walletAddress;

    public CryptoPaymentStrategy(String walletAddress) {
        this.walletAddress = walletAddress;
    }

    @Override
    public void pay(double amount) {
        double fee = calculateFee(amount);
        // Crypto has network fees but we pass savings on to the user
        System.out.printf("[Crypto] Sending $%.2f (network fee: $%.2f) "
                + "to wallet %s — awaiting blockchain confirmation%n",
                amount, fee, walletAddress);
    }

    @Override
    public double calculateFee(double amount) {
        return 0.50; // flat $0.50 network fee regardless of amount
    }
}

// ── STEP 5: The Context — OrderProcessor ───────────────────────────────────
// This class has NO idea which payment method is being used.
// It just calls pay() and the strategy handles everything.
class OrderProcessor {

    // Holds a reference to the interface — NOT a concrete class
    private PaymentStrategy paymentStrategy;

    // Strategy is injected via the constructor (constructor injection)
    public OrderProcessor(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    // You can also swap the strategy at runtime — powerful for multi-step checkouts
    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void checkout(double orderTotal) {
        System.out.println("\n=== Processing Order: $" + orderTotal + " ===");
        paymentStrategy.pay(orderTotal); // delegate — don't care HOW it's done
        System.out.println("Order confirmed. Receipt sent.");
    }
}

// ── STEP 6: Wire it all together ───────────────────────────────────────────
public class StrategyPatternPayment {

    public static void main(String[] args) {

        // Customer A pays by credit card
        PaymentStrategy creditCard = new CreditCardPaymentStrategy("Alice Johnson", "4242");
        OrderProcessor aliceCheckout = new OrderProcessor(creditCard);
        aliceCheckout.checkout(120.00);

        // Customer B pays via PayPal
        PaymentStrategy paypal = new PayPalPaymentStrategy("bob@example.com");
        OrderProcessor bobCheckout = new OrderProcessor(paypal);
        bobCheckout.checkout(75.00);

        // Customer C switches from PayPal to crypto mid-session (runtime swap!)
        PaymentStrategy crypto = new CryptoPaymentStrategy("0xABCD1234EF");
        bobCheckout.setPaymentStrategy(crypto); // hot-swap the strategy
        bobCheckout.checkout(75.00);
    }
}
Output
=== Processing Order: $120.0 ===
[Credit Card] Charging $120.00 (+ $1.80 fee = $121.80 total) to card ending in 4242 for Alice Johnson
Order confirmed. Receipt sent.
=== Processing Order: $75.0 ===
[PayPal] Redirecting bob@example.com to PayPal for $75.00 (+ $2.18 PayPal fee = $77.18 total)
Order confirmed. Receipt sent.
=== Processing Order: $75.0 ===
[Crypto] Sending $75.00 (network fee: $0.50) to wallet 0xABCD1234EF — awaiting blockchain confirmation
Order confirmed. Receipt sent.
Pro Tip:
Notice that OrderProcessor never imports or references CreditCardPaymentStrategy, PayPalPaymentStrategy, or CryptoPaymentStrategy directly. It only knows about the PaymentStrategy interface. This is the key: you could ship OrderProcessor as a compiled library JAR and users could add new payment strategies without touching your code at all.
Production Insight
Constructor injection is the safest way to supply strategies.
Setter injection allows runtime swaps but makes the object mutable – test both paths.
If a strategy must be shared across threads, ensure it's stateless or properly synchronised.
Key Takeaway
Context should receive a strategy, never create or select one.
Interface reference is all the Context needs.
Rule: if a strategy is passed in, the pattern is intact; if it's looked up inside, it's broken.

Strategy vs Inheritance — Why Composition Wins Here

The most common question when first seeing the Strategy Pattern is: 'Why not just subclass OrderProcessor and override the checkout method?' It's a fair question. Inheritance feels natural for this kind of variation.

Here's why it falls apart: with inheritance, each subclass carries the entire OrderProcessor with it. If a customer wants to change payment method mid-session — say they start with PayPal and switch to crypto — you'd need to replace the entire object, not just swap one behaviour. Inheritance bakes behaviour into the class hierarchy at compile time. Strategy swaps it at runtime.

Inheritance also creates a rigid tree. What if a processor needs to combine behaviours — say, a payment strategy AND a discount strategy? Multiple inheritance isn't available in Java, and deep hierarchies become a maintenance nightmare. Composition ('has-a') is almost always more flexible than inheritance ('is-a') when the varying behaviour needs to be independently changeable. The Gang of Four literally coined the phrase 'favour composition over inheritance' — the Strategy Pattern is the canonical example of why.

StrategyWithLambda.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// BONUS: In Java 8+, if your Strategy interface has ONE method,
// you can use lambdas as lightweight strategies — no boilerplate class needed.
// This is perfect for simple, one-off strategies defined at the call site.

@FunctionalInterface // marks this as safe to use with lambdas
interface DiscountStrategy {\n    double apply(double originalPrice);\n}

class PricingEngine {

    private DiscountStrategy discountStrategy;

    public PricingEngine(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    public double calculateFinalPrice(double originalPrice) {
        return discountStrategy.apply(originalPrice);
    }
}

public class StrategyWithLambda {

    public static void main(String[] args) {

        // Strategy 1: 10% off for loyal customers — defined inline as a lambda
        PricingEngine loyalCustomerPricing = new PricingEngine(
                price -> price * 0.90  // the lambda IS the strategy
        );

        // Strategy 2: Flat $5 off for newsletter subscribers
        PricingEngine newsletterPricing = new PricingEngine(
                price -> Math.max(0, price - 5.00) // never go below $0
        );

        // Strategy 3: No discount for regular customers
        PricingEngine regularPricing = new PricingEngine(
                price -> price // identity — no change
        );

        double basePrice = 45.00;

        System.out.printf("Base price:               $%.2f%n", basePrice);
        System.out.printf("Loyal customer price:     $%.2f%n", loyalCustomerPricing.calculateFinalPrice(basePrice));
        System.out.printf("Newsletter subscriber:    $%.2f%n", newsletterPricing.calculateFinalPrice(basePrice));
        System.out.printf("Regular customer price:   $%.2f%n", regularPricing.calculateFinalPrice(basePrice));
    }
}
Output
Base price: $45.00
Loyal customer price: $40.50
Newsletter subscriber: $40.00
Regular customer price: $45.00
Interview Gold:
When an interviewer asks 'how is the Strategy Pattern different from using inheritance?', the killer answer is: 'Strategy uses composition to vary behaviour at runtime; inheritance locks behaviour into the class hierarchy at compile time. Strategy also follows the Single Responsibility Principle — each algorithm lives in its own class — whereas inheritance tends to push multiple concerns into one hierarchy.'
Production Insight
Inheritance hierarchies become rigid over time — adding a new behaviour often requires refactoring the base class.
Composition with strategies keeps the base dead simple.
Real-world example: a reporting engine that needs different formatters (CSV, PDF, HTML) — inheritance forces a new subclass per formatter per report type, composition just swaps the formatter strategy.
Key Takeaway
Favour composition over inheritance when behaviour varies independently.
Strategy swaps at runtime; inheritance locks at compile time.
Rule: if a future requirement might need to combine behaviours, composition is your only sane path.

Selecting Strategies with Enums and Factories – Clean Wiring

Once you've extracted strategies, the next natural question is: how does the client decide which concrete strategy to use? The answer must never be a switch statement inside the Context. Instead, use a StrategyFactory that maps a selection key (usually an enum) to the appropriate concrete strategy. The factory can be configured with a Map<PaymentType, PaymentStrategy> that gets populated at startup — either hardcoded or injected via configuration.

This separation keeps the selection logic isolated and open for extension. Adding a new payment type means: 1) add a new enum constant, 2) write the strategy class, 3) register it in the factory map. The Context and the factory's core logic never change.

An alternative approach for simpler cases: use a static factory method or a dedicated Config class that builds the strategy map from a configuration file. This is especially useful when the set of strategies is handed by your operations team without a code deployment.

StrategyFactoryExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Clean strategy selection using an enum and a factory map

enum PaymentType {
    CREDIT_CARD, PAYPAL, CRYPTO, APPLE_PAY
}

class PaymentStrategyFactory {\n\n    private final Map<PaymentType, PaymentStrategy> strategies = new HashMap<>();\n\n    // Register strategies – called once at application start\n    public void register(PaymentType type, PaymentStrategy strategy) {\n        strategies.put(type, strategy);\n    }

    // Retrieve strategy – never returns null; fails fast with clear message
    public PaymentStrategy getStrategy(PaymentType type) {
        PaymentStrategy strategy = strategies.get(type);
        if (strategy == null) {
            throw new IllegalArgumentException(
                "No strategy registered for payment type: " + type
            );
        }
        return strategy;
    }
}

// Usage in main / configuration
PaymentStrategyFactory factory = new PaymentStrategyFactory();
factory.register(PaymentType.CREDIT_CARD, new CreditCardPaymentStrategy("N/A", "0000"));
factory.register(PaymentType.PAYPAL, new PayPalPaymentStrategy("default@example.com"));
factory.register(PaymentType.CRYPTO, new CryptoPaymentStrategy("0x0"));

// At checkout time – no switch, no if-else
OrderProcessor processor = new OrderProcessor(factory.getStrategy(PaymentType.PAYPAL));
processor.checkout(99.99);
Output
=== Processing Order: $99.99 ===
[PayPal] Redirecting default@example.com to PayPal for $99.99 (+ $2.90 PayPal fee = $102.89 total)
Order confirmed. Receipt sent.
Pro Tip:
Use a factory with a Map to avoid any switch. The factory itself is open for extension – you can add new strategies without modifying the factory class by making the map injectable from a configuration source like Spring beans or a JSON file.
Production Insight
A factory that uses a Map makes the selection logic testable and extensible.
If you use a ServiceLoader or Spring @Component, registration becomes automatic.
Don't forget to handle null strategy gracefully – a missing registration should log and fail fast, not silently return null.
Key Takeaway
Separate strategy selection from both the Context and the strategies.
A factory with a Map<Enum, Strategy> is cleaner than any switch.
Rule: the only place an 'if' or 'switch' for strategy selection is acceptable is inside the factory – nowhere else.

Strategy Pattern in the Java Standard Library: Comparator, HttpServlet, and Filter

The Java standard library itself uses the Strategy Pattern in several core APIs. Recognising these examples reinforces that the pattern is not just academic — it's a proven design used extensively by the JDK authors.

JavaStandardLibraryExamples.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Example: Comparator as a Strategy (Built-in Strategy Pattern in JDK)
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// Natural order strategy
Collections.sort(names);

// Custom strategy: sort by length using lambda
Collections.sort(names, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

// Example: Functional interfaces as strategies (Java 8+)
UnaryOperator<String> upperCase = String::toUpperCase;
UnaryOperator<String> addExclamation = s -> s + "!";

// Composing strategies
UnaryOperator<String> shout = upperCase.andThen(addExclamation);

System.out.println(shout.apply("hello")); // Output: HELLO!
Output
HELLO!
Real-World Recognition:
Whenever you see an interface with a single method that gets passed around (especially in older Java), it's likely an implementation of the Strategy Pattern. The Gang of Four's pattern is deeply baked into the language design.
Production Insight
Understanding that Comparator is a Strategy helps you write cleaner code: instead of creating many sorting utility methods, define a Comparator and pass it to the sorting framework. This is exactly the Strategy Pattern in daily Java practice.
Key Takeaway
The Java standard library is a rich source of Strategy Pattern examples. If you use Comparator, Function, HttpServlet, or Filters, you're already applying the pattern — even if you didn't call it by name.

Function Composition: Combining Strategies with reduce()

One of the most powerful modern applications of the Strategy Pattern in Java is function composition using Java 8+ features. Instead of creating many individual strategy classes, you can create small, focused UnaryOperator<T> strategies and compose them into powerful pipelines.

For example, consider a pricing engine that applies multiple discounts in sequence: a percentage discount, a flat coupon, and a loyalty bonus. You can treat each discount as a separate strategy and combine them dynamically.

FunctionCompositionStrategy.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.function.UnaryOperator;
import java.util.stream.Stream;

public class FunctionCompositionStrategy {

    // Each discount is a small, focused strategy
    static UnaryOperator<Double> tenPercentOff = price -> price * 0.9;
    static UnaryOperator<Double> fiveDollarCoupon = price -> Math.max(0, price - 5.0);
    static UnaryOperator<Double> loyaltyBonus = price -> price > 100 ? price - 20 : price;

    public static void main(String[] args) {
        double basePrice = 150.00;

        // Combine all discount strategies into one composite strategy
        UnaryOperator<Double> allDiscounts = Stream.of(
                tenPercentOff,
                fiveDollarCoupon,
                loyaltyBonus
            ).reduce(UnaryOperator.identity()
Output
Base price: $150.00
Final price after all discounts: $105.00
Why This Matters:
When strategies are pure functions (no side effects), composition becomes trivial. You can build complex business logic from small, reusable, and easily testable building blocks. This is a modern, functional take on the Strategy Pattern.
Production Insight
In production systems, composable strategies are extremely valuable. You can read a list of rules or discounts from a database or configuration and compose them at runtime. This approach is far more maintainable than deeply nested if-else or switch statements.
Key Takeaway
With UnaryOperator.andThen() and reduce(), you can compose multiple strategies into a single pipeline. This gives you enormous flexibility while keeping your code clean and extensible.

Strategy Pattern in Python — Lambda-Based Strategies

Python's dynamic nature and first-class functions make the Strategy Pattern almost trivially simple. There is no need for separate strategy classes or interfaces — you can pass any callable (function, lambda, or class with __call__) directly as a strategy. This demonstrates the essence of the pattern stripped of boilerplate.

The example below implements the same payment processing system in Python, using functions as strategies and a context that expects a callable. Notice how adding a new payment method is just defining a new function — no classes involved unless you need state.

strategy_payments.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Python Strategy Pattern using functions (no interfaces, no boilerplate)

def credit_card_payment(amount: float):
    fee = amount * 0.015
    total = amount + fee
    print(f"[Credit Card] Charged ${amount:.2f} (+ ${fee:.2f} fee = ${total:.2f})")

def paypal_payment(amount: float):
    fee = amount * 0.029
    total = amount + fee
    print(f"[PayPal] Charged ${amount:.2f} (+ ${fee:.2f} fee = ${total:.2f})")

def crypto_payment(amount: float):
    fee = 0.50
    total = amount + fee
    print(f"[Crypto] Charged ${amount:.2f} (+ ${fee:.2f} fee = ${total:.2f})")

# Context - expects a callable strategy
class OrderProcessor:
    def __init__(self, payment_strategy):
        self._payment_strategy = payment_strategy

    def set_strategy(self, payment_strategy):
        self._payment_strategy = payment_strategy

    def checkout(self
Output
=== Processing Order: $120.00 ===
[Credit Card] Charged $120.00 (+ $1.80 fee = $121.80)
Order confirmed.
=== Processing Order: $75.00 ===
[PayPal] Charged $75.00 (+ $2.18 fee = $77.18)
Order confirmed.
=== Processing Order: $75.00 ===
[Crypto] Charged $75.00 (+ $0.50 fee = $75.50)
Order confirmed.
=== Processing Order: $99.99 ===
[Apple Pay] Charged $99.99
Order confirmed.
Python Idiom:
In Python, the Strategy Pattern collapses to passing a function. There's no need for interface or abstract class. This is a perfect illustration that the pattern is about intent — not ceremony.
Production Insight
Python's duck typing means you can use any callable as a strategy. In production, you often see strategies read from a configuration file or a registry of registered functions. The flexibility comes with the responsibility of ensuring the callable signature matches — a runtime error if not.
Key Takeaway
Python's first-class functions make the Strategy Pattern invisible — the pattern is simply passing functions as arguments. The core idea of interchangeable behaviour remains the same.

Advantages and Disadvantages of the Strategy Pattern

Understanding the trade-offs is what separates pattern knowledge from pattern wisdom. The Strategy pattern solves real problems but introduces real costs — knowing both prevents over-engineering and misapplication.

Advantages:

Open/Closed compliance — you add new strategies (new algorithm variants) without modifying the Context class. A payments system that supports PayPal, Stripe, and ApplePay can add CryptoPay by writing one new class, with zero changes to existing code.

Eliminates conditional bloat — a Context class with a 200-line if/else chain for algorithm selection becomes a Context class with a single strategy.execute() call. Cyclomatic complexity drops from O(n algorithms) to O(1).

Testability — each strategy is an isolated unit. You can test PayPalStrategy and StripeStrategy independently with no Context setup. You can inject a MockStrategy into Context to verify routing logic without triggering real payment calls.

Runtime switching — Context can swap strategies on a per-request basis. A sort function can choose QuickSort for large inputs and InsertionSort for small inputs based on runtime conditions, without the caller knowing.

Disadvantages:

Class proliferation — every algorithm variant becomes a class. A system with 12 sorting strategies has 12 files. For simple cases where a lambda or a Comparator would suffice, this is engineering overhead that makes the codebase harder to navigate.

Client awareness — the caller must know which strategy to select and inject. This moves decision logic from the Context into whatever code creates the Context, which is not always an improvement. If selection logic is complex, it belongs in a Factory or a registry, not scattered across callers.

Interface rigidity — all strategies must conform to the same interface. If strategies need meaningfully different signatures (different parameters, different return types), the interface becomes a lowest-common-denominator contract that forces awkward parameter packaging.

When NOT to use Strategy:

If you have two algorithms and they will never grow to three, an if/else is cleaner than a Strategy pattern. The pattern pays off when the algorithm set is open-ended or when independent testability is a hard requirement.

NotApplicable.javaJAVA
1
// No code needed for this conceptual summary
Forge Perspective
Patterns aren't goals, they are solutions. Apply the Strategy pattern only when you see logic changing frequently or when you need to unit test algorithms independently.
Production Insight
Beware of 'Architecture Astronaut' syndrome. If your logic only has two branches and will never have three, a simple if-else is perfectly fine. Don't build a factory for a problem that doesn't exist.
Key Takeaway
Strategy eliminates conditional branching and enables open/closed extension — but every strategy becomes a class, so apply it when the algorithm set is genuinely open-ended.

Pros and Cons of the Strategy Pattern

Here is a quick reference table of the advantages and disadvantages discussed above:

AdvantageDisadvantage
Open/Closed Principle – Add new algorithms without modifying existing codeClass proliferation – Each algorithm becomes a separate class; can increase project size significantly
Eliminates conditional logic – No switch/if-else chains in the ContextClient awareness – The caller must know which strategy to choose, shifting complexity outward
Independent testability – Each strategy can be unit tested in isolationInterface rigidity – All strategies must conform to the same interface, which may force compromises
Runtime switching – The Context can be reconfigured at runtime (e.g., per request)Overhead for simple cases – Two fixed algorithms don't need a full pattern; a simple conditional is more direct
Single Responsibility – Each class has one job: implement one algorithm variantHidden dependencies – If not using a factory, injection points can become scattered

The table shows that the Strategy Pattern's strengths are most valuable when you expect the set of algorithms to grow over time or when you need maximum flexibility. Its weaknesses matter most in small, controlled codebases where simplicity is king.

noneTEXT
1
Decision Helper
Before applying the Strategy Pattern, ask: 'Will I add a third variant of this algorithm within the next release cycle?' If no, keep it simple.
Production Insight
In enterprise applications, the Strategy Pattern is often the right choice for payment methods, authentication providers, and export formatters. The initial investment pays back quickly when the business adds its fifth or sixth variant.
Key Takeaway
The Strategy Pattern's pros outweigh its cons when algorithms are numerous, open-ended, or require independent evolution.

When to Use the Strategy Pattern — Applicability Checklist

Not every situation calls for the Strategy Pattern. Use this checklist to determine if your codebase will benefit from it:

☐ You have multiple classes that differ only in their behaviour. If you see several classes that share the same structure but implement a method differently, that's a clear signal to extract the varying behaviour into strategies.

☐ You need to swap behaviour at runtime. If the algorithm a Context uses depends on user input, configuration, or runtime conditions, Strategy lets you change it without conditional logic.

☐ You want to isolate algorithms for testing. If you need to write dedicated unit tests for each algorithm variant, Strategy gives you that separation.

☐ The algorithm set is expected to grow. If your roadmap includes adding more variants (new payment methods, new export formats, new sorting criteria), Strategy prevents modifying existing code.

☐ Conditional logic is scattered and hard to maintain. If you find yourself adding if blocks in multiple places to handle the same variation, Strategy consolidates it.

☐ The algorithm is used in more than one Context. If the same algorithm appears in different classes, extracting it as a strategy promotes reuse.

If you checked three or more boxes, the Strategy Pattern is a good fit. If you checked one or two, consider a simpler approach like a lambda or a Comparator.

no_code_needed.javaJAVA
1
// No code — this is a decision guide
Forge Mindset
Patterns are solutions to recurring problems. Don't start with the pattern; start with the problem. The checklist helps you recognise when the problem is present.
Production Insight
In a real code review, if you see a developer introducing Strategy for a single if-else branch, push back. If you see them refactoring a 20-branch switch into strategies, celebrate. The checklist separates healthy pattern use from over-engineering.
Key Takeaway
Use the Strategy Pattern when behaviour varies independently and the set of variants is open-ended. For fixed, small sets, simpler alternatives exist.

Relations with Other Patterns: Strategy vs State, Command, Template Method

The Strategy Pattern is often compared to other behavioural patterns. Understanding the distinctions prevents misapplication.

Strategy vs State Pattern Both use composition and delegate to another object. However, State changes behaviour based on internal state transitions — the current state object itself decides which state comes next. In Strategy, the Context never changes its strategy automatically; an external caller swaps it. State is about changing behaviour as the object's state changes; Strategy is about offering interchangeable, independent algorithms. Example: A TCP connection uses State — the connection object changes its behaviour (open, listening, closed) based on internal state. A payment processor uses Strategy — the processor doesn't decide which payment method to use; the caller injects it.

Strategy vs Command Pattern Command encapsulates a request as an object, often for queuing, logging, or undo. Each command usually has a single operation (execute/undo). Strategy encapsulates an algorithm, which may involve multiple steps. Command is about delayed execution and parameterisation; Strategy is about selecting an implementation. They can complement each other: a Command can use a Strategy to perform its work. Example: A menu item in a GUI uses Command to trigger an action (Save, Print). A text editor uses Strategy to implement different compression algorithms (ZIP, GZIP).

Strategy vs Template Method Pattern Template Method defines the skeleton of an algorithm in a base class and lets subclasses override specific steps. It uses inheritance. Strategy uses composition and lets the entire algorithm be swapped. Template Method is appropriate when the algorithm structure is fixed but some steps vary. Strategy is for when you need to swap the entire algorithm. Example: A data mining framework uses Template Method to define a fixed pipeline (extract, transform, load) where only the data source varies. A sorting library uses Strategy because the entire sorting algorithm (quicksort, mergesort) is interchangeable.

In summary: State changes behaviour automatically; Command packages a request; Template Method fixes the skeleton via inheritance; Strategy swaps the whole algorithm via composition. Knowing these relations helps you pick the right pattern.

RelationsIllustration.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Illustrative pseudocode for each pattern's relationship context

// --- State: Object changes its own behaviour ---
class TcpConnection {
    private TcpState state;
    void open() { state.open(this); }   // state transitions internally
    void close() { state.close(this); }
}

// --- Command: Encapsulates a request ---
interface Command {
    void execute();
    void undo();
}
class SaveCommand implements Command { /* ... */ }

// --- Template Method: Fixed skeleton, overridable steps ---
abstract class DataMiner {\n    public final void mine() {\n        extract();\n        transform();\n        load();\n    }
    abstract void extract();
    abstract void transform();
    void load() { /* default */ }
}

// --- Strategy: Swappable algorithm ---
interface SortStrategy {
    void sort(int[] data);
}
class QuickSort implements SortStrategy { /* ... */ }
class MergeSort implements SortStrategy { /* ... */ }
Key Distinction
Strategy and State look similar in UML (both use composition) but differ in intent: State encapsulates state-bound behaviour with automatic transitions; Strategy encapsulates interchangeable algorithms selected externally.
Production Insight
In large codebases, you often see patterns combined. For example, a Command object may receive a Strategy to perform its logic, and that Strategy may itself be a State machine. Understanding the relations helps you read and refactor existing code confidently.
Key Takeaway
Strategy differs from State (autonomy vs. external selection), Command (single request vs. algorithm), and Template Method (inheritance vs. composition). Use Strategy when the whole algorithm needs to be swapped.

Production Pitfalls: Thread Safety, State, and Testing Strategies

The Strategy Pattern looks simple in examples, but production systems reveal two common pitfalls. First: shared mutable state. If a strategy maintains internal state (e.g., a counter, a cached token), and the same strategy instance is shared across multiple threads, you get race conditions. Strategy implementations should ideally be stateless – any method parameters should be passed in rather than stored as instance variables. If state is unavoidable, use carefully scoped instances per thread or synchronisation.

Second: testing strategies in isolation. The pattern's main advantage is testability, but only if you test each concrete strategy independently. A common mistake is to test only the Context with a mock strategy, assuming the concrete strategies work. Write dedicated unit tests for each strategy with realistic inputs and edge cases (null amounts, zero amounts, network timeouts). For strategies that call external services (payment gateways), use mocks for the external dependency and test the strategy's behaviour in isolation.

Finally, watch out for configuration-driven strategy selection that fails silently. If you read strategy names from a properties file and instantiate via reflection, a typo will create a NullPointerException or a ClassNotFoundException at runtime. Validate the configuration at application startup – fail fast rather than discover the error when a real customer tries to pay.

ThreadSafetyAndTesting.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Example of a stateless strategy – safe to share across threads
class StatelessCryptoPaymentStrategy implements PaymentStrategy {
    // No instance fields – all state comes from method parameters
    @Override
    public void pay(double amount) {
        System.out.println("Processing crypto payment of $" + amount);
    }

    @Override
    public double calculateFee(double amount) {
        return 0.50; // pure function
    }
}

// Unit test for a strategy – isolated, no Context needed
public class CreditCardPaymentStrategyTest {\n\n    @Test\n    void testPayWithStandardAmount() {\n        PaymentStrategy strategy = new CreditCardPaymentStrategy(\"John\", \"1234\");\n        // Capture output with System.setOut or verify side effect\n        // For simplicity, just check no exception is thrown\n        assertDoesNotThrow(() -> strategy.pay(100.00));\n    }\n\n    @Test\n    void testFeeCalculation() {\n        PaymentStrategy strategy = new CreditCardPaymentStrategy(\"John\", \"1234\");\n        assertEquals(1.50, strategy.calculateFee(100.00), 0.001);\n    }\n\n    @Test\n    void testZeroAmount() {\n        PaymentStrategy strategy = new CreditCardPaymentStrategy(\"John\", \"1234\");\n        assertDoesNotThrow(() -> strategy.pay(0.0));\n    }"
      }
● Production incidentPOST-MORTEMseverity: high

The Missing Payment Strategy That Took Down Checkout

Symptom
Customers trying to pay with 'Apple Pay' received an IllegalArgumentException: 'Unknown payment type: APPLE_PAY'. The checkout flow was completely blocked for all Apple Pay users for 47 minutes.
Assumption
The team assumed that adding a new payment method only required implementing a new strategy class. They didn't realise the Context contained a switch statement that mapped payment type strings to strategy instances.
Root cause
The OrderProcessor class had a private method getStrategy(String type) that used a switch statement to instantiate the correct PaymentStrategy. This violated the fundamental rule of the Strategy Pattern: the Context must never contain strategy-selection logic. The switch was an invisible dependency that had to be updated every time a new strategy was added.
Fix
Removed the switch statement from OrderProcessor. Moved strategy selection to a factory class (PaymentStrategyFactory) that could accept new strategies via a registry pattern. The Context now receives an already-constructed strategy via constructor injection, never makes a decision about which strategy to use.
Key lesson
  • The Context must never contain logic to choose between strategies – that selection belongs in a factory, configuration, or calling code.
  • Review all existing Context classes for any remaining if-else or switch that defeats the pattern's purpose.
  • Add a unit test that verifies a new strategy can be injected without modifying the Context.
  • Use compile-time or configuration-driven selection to eliminate the risk of missing mappings.
Production debug guideSymptom → Action guide for common strategy wire-up failures3 entries
Symptom · 01
NullPointerException when calling strategy method
Fix
Check that the strategy field is properly injected. If using constructor injection, verify the constructor argument is not null. If using setter injection, ensure the setter is called before any operation.
Symptom · 02
Wrong strategy is being executed (e.g., credit card logic runs when PayPal was expected)
Fix
Inspect the calling code that creates the strategy instance. It's likely the strategy selection logic (if-else or switch) is inside the Context or calling code. Move selection to a factory and verify the correct instance is passed.
Symptom · 03
Adding a new strategy class doesn't take effect, no error thrown
Fix
Check if the selection logic (switch, if-else, or enum mapping) was updated. If the selection is inside the Context, it's missing the new case. If using a factory with a registry, verify the new strategy is registered.
★ Quick Cheat Sheet: Strategy Pattern Wire-upThree common misconfigurations and how to fix them fast, without digging through logs.
NullPointerException on strategy method call
Immediate action
Check the Context's constructor or setter for the strategy field. Is it assigned before use?
Commands
grep 'private.*Strategy' *Processor.java
grep -A5 'setPaymentStrategy\|new.*Strategy' callingClass.java
Fix now
Add a null check and a default strategy (or fail fast with a clear message).
Strategy selected incorrectly at runtime+
Immediate action
Find where the strategy instance is created – is there a switch/if-else inside the Context?
Commands
grep -n 'if\|switch' OrderProcessor.java
grep -r 'implements PaymentStrategy' src/
Fix now
Move selection logic to a StrategyFactory class and inject the factory into the Context.
New strategy class exists but is never used+
Immediate action
Check if the selection mechanism lists the new type (enum, config, switch).
Commands
grep -R 'PAYMENT_TYPE' config/
grep 'case .*:' StrategySelector.java
Fix now
Register the new strategy in the factory or add the enum constant.
🔥

That's Advanced Java. Mark it forged?

12 min read · try the examples if you haven't

Previous
Observer Pattern in Java
16 / 28 · Advanced Java
Next
Decorator Pattern in Java