Back to SOLID Principles
Q3Question 3 of 5

What is the Strategy pattern and how does it enforce the Open/Closed Principle

Key Concepts
  • Strategy defines a family of interchangeable algorithms behind one interface, chosen at runtime.
  • Three parts: strategy interface, concrete strategies, and a context that holds and delegates to a strategy.
  • Enforces OCP: adding a new algorithm is a new class, never an edit to existing code.
  • Java standard library examples: Comparator, RejectedExecutionHandler, Collector.
  • In modern Java, a single-method strategy is a functional interface, so a lambda replaces a named class.
  • In Spring, injecting a Map of bean name to strategy gives runtime selection with zero factory code.

Definition

Strategy makes algorithms interchangeable behind one interface, chosen at runtime, so adding a new algorithm is a new class, never an edit to old code.
  • Strategy pattern – defines a family of interchangeable algorithms behind a common interface and lets the algorithm be selected at runtime.
  • Context – holds a reference to the strategy interface and delegates to whichever concrete strategy it was given, without knowing which one.
  • Open/Closed – adding a new algorithm means adding a new strategy class rather than editing existing code.
  • Functional fit – in Java, a single-method strategy interface is a @FunctionalInterface, so a lambda or method reference can replace a named class.

When Rajesh pays at the National Bank of India, he can choose UPI, card, or net banking. The payment counter does not care which he picks; it just runs whatever payment method he hands it. Each payment method is a self-contained strategy, and adding a new one like a wallet means opening a new method lane without touching the existing lanes. The counter stays the same; only the chosen method changes.

Understanding the Concepts

What is the Strategy pattern?

Strategy has three parts: a strategy interface that declares the algorithm, concrete strategies that implement it differently, and a context that holds a strategy and delegates to it. The context depends only on the interface, so you can swap strategies without changing the context.

JAVA
package com.bank.service;

import java.math.BigDecimal;

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

// Concrete strategies
public class UpiPayment implements PaymentStrategy {
    public void pay(BigDecimal amount) {
        System.out.println("Paid " + amount + " via UPI");
    }
}

public class CardPayment implements PaymentStrategy {
    public void pay(BigDecimal amount) {
        System.out.println("Paid " + amount + " via Card");
    }
}

public class NetBankingPayment implements PaymentStrategy {
    public void pay(BigDecimal amount) {
        System.out.println("Paid " + amount + " via Net Banking");
    }
}

// Context
public class PaymentContext {
    private PaymentStrategy strategy;

    public PaymentContext(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

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

    public void checkout(BigDecimal amount) {
        strategy.pay(amount);
    }
}

How does Strategy enforce the Open/Closed Principle?

Open for extension means you can add a new payment method. Closed for modification means you do not touch the tested code to do it. Without Strategy, a payment method would be a growing switch.

JAVA
package com.bank.service;

import java.math.BigDecimal;

// VIOLATION of OCP: every new method edits this switch
public class PaymentProcessorBad {
    public void pay(String type, BigDecimal amount) {
        if ("UPI".equals(type)) {
            System.out.println("Paid via UPI: " + amount);
        } else if ("CARD".equals(type)) {
            System.out.println("Paid via Card: " + amount);
        }
        // adding WALLET means editing here
    }
}

With Strategy, adding WalletPayment is a new class that implements PaymentStrategy. The context and existing strategies are never touched.

JAVA
// New strategy – zero edits to existing code
public class WalletPayment implements PaymentStrategy {
    public void pay(BigDecimal amount) {
        System.out.println("Paid " + amount + " via Wallet");
    }
}

Where does Strategy appear in the Java standard library?

Strategy is everywhere in the JDK:

  • Comparator is the clearest example. list.sort(Comparator.comparing(Account::getBalance)) plugs a comparison algorithm into a fixed sort.
  • RejectedExecutionHandler in ThreadPoolExecutor is a strategy for what to do when the queue is full.
  • Collector in the Streams API is a strategy for how to accumulate stream elements.
  • Runnable and Callable passed to a thread or executor are strategies for what work to do.
JAVA
package com.bank.demo;

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class StrategyInJdk {
    record Account(String name, BigDecimal balance) {}

    public static void main(String[] args) {
        List<Account> accounts = Arrays.asList(
            new Account("Rajesh", new BigDecimal("150000")),
            new Account("Priya", new BigDecimal("250000")),
            new Account("Pankaj", new BigDecimal("50000"))
        );

        // Comparator is a Strategy
        accounts.sort(Comparator.comparing(Account::balance));
        accounts.forEach(a -> System.out.println(a.name() + ": " + a.balance()));
    }
}
TEXT
Pankaj: 50000
Rajesh: 150000
Priya: 250000

How do you implement Strategy with lambdas in modern Java?

A single-method interface is a @FunctionalInterface, so a strategy can be a lambda or method reference instead of a named class.

JAVA
package com.bank.demo;

import java.math.BigDecimal;

public class StrategyLambdaDemo {
    @FunctionalInterface
    interface InterestStrategy {
        BigDecimal calculate(BigDecimal balance);
    }

    public static void main(String[] args) {
        // Strategies as lambdas – no named class needed
        InterestStrategy savings = balance -> balance.multiply(new BigDecimal("0.04"));
        InterestStrategy current = balance -> balance.multiply(new BigDecimal("0.02"));
        InterestStrategy nre     = balance -> balance.multiply(new BigDecimal("0.065"));

        BigDecimal balance = new BigDecimal("500000");
        System.out.println("Savings interest: " + savings.calculate(balance));
        System.out.println("Current interest: " + current.calculate(balance));
        System.out.println("NRE interest:     " + nre.calculate(balance));
    }
}
TEXT
Savings interest: 20000.000
Current interest: 10000.000
NRE interest:     32500.000

How does Strategy work with Spring injection?

In Spring, injecting a Map gives runtime selection by bean name for free.

JAVA
package com.bank.service;

import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.Map;

@Component
public class PaymentService {
    private final Map<String, PaymentStrategy> strategies;

    // Spring injects all PaymentStrategy beans, keyed by bean name
    public PaymentService(Map<String, PaymentStrategy> strategies) {
        this.strategies = strategies;
    }

    public void pay(String method, BigDecimal amount) {
        PaymentStrategy strategy = strategies.get(method);
        if (strategy == null) {
            throw new IllegalArgumentException("Unknown payment method: " + method);
        }
        strategy.pay(amount);
    }
}

Adding a new payment method is literally adding a new @Component bean. No factory edit, no switch statement.

How is Strategy different from Template Method?

Strategy selects the entire algorithm at runtime via composition. Template Method defines a skeleton in a base class and lets subclasses override specific steps via inheritance.

| Aspect | Strategy | Template Method |
|---|---|---|
| Mechanism | Composition (has-a) | Inheritance (is-a) |
| Selection | Runtime, swap freely | Compile time, fixed by subclass |
| Granularity | Whole algorithm | Individual steps |
| Coupling | Loose | Tight (subclass to parent) |

JAVA
// Strategy: compose at runtime
PaymentContext ctx = new PaymentContext(new UpiPayment());
ctx.checkout(new BigDecimal("5000"));
ctx.setStrategy(new CardPayment()); // swap at runtime
ctx.checkout(new BigDecimal("3000"));

// Template Method: fixed by subclass at compile time
public abstract class LoanProcessor {
    public final void process() {
        validate();
        calculateEmi();  // subclass provides this step
        disburse();
    }
    protected abstract void calculateEmi();
    // ...
}

What are common mistakes with Strategy?

DIAGRAM
- Using Strategy for a single, fixed algorithm that will never vary. A plain method is simpler.
- Creating a strategy hierarchy when a simple lambda suffices. In modern Java, `amount -> amount.multiply(rate)` beats a named class for trivial logic.
- Forgetting null checks on the strategy reference, which causes `NullPointerException` at runtime.

What should a senior engineer know?

  • Strategy is the pattern Rajesh reaches for most often because it directly enforces OCP.
  • In a banking system, strategies absorb regulatory changes: new interest rules, new fee types, new payment methods.
  • The Map pattern in Spring replaces the classic Factory plus Strategy combination.
  • Strategy works best when the number of algorithms is open-ended. For two fixed variants, a simple if-else is simpler and honest.

Visual Overview

DIAGRAM
  Strategy Pattern Structure
  ──────────────────────────

  ┌──────────────────────┐
  │    PaymentContext     │
  │ ─────────────────     │
  │ - strategy: Strategy  │
  │ + checkout(amount)    │──── delegates ────▶ strategy.pay(amount)
  └──────────────────────┘
           │
           │ holds reference to
           ▼
  ┌──────────────────────┐
  │  PaymentStrategy     │ ◄── interface
  │  + pay(amount)       │
  └──────────┬───────────┘
             │
     ┌───────┼────────┬──────────┐
     ▼       ▼        ▼          ▼
  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
  │ UPI  │ │ Card │ │ Net  │ │Wallet│
  │      │ │      │ │ Bank │ │ (new)│
  └──────┘ └──────┘ └──────┘ └──────┘

  Adding Wallet = new class only
  Context + existing strategies: UNTOUCHED

Key Takeaway

The Strategy pattern defines a family of interchangeable algorithms behind a common interface and lets Rajesh choose the algorithm at runtime. It has three parts: a strategy interface that declares the algorithm, concrete strategies that implement it differently, and a context that holds a strategy and delegates to it without knowing which concrete one it has. The whole point is that the context depends only on the interface, so algorithms swap freely.

It appears all over the Java standard library. The clearest example is Comparator, where Rajesh plugs a comparison algorithm into a fixed sort. The thread pool's RejectedExecutionHandler is a strategy for what to do when the queue is full, the Collector in the Streams API is a strategy, and even a Runnable handed to a thread is a pluggable unit of behaviour.

Strategy enforces the Open/Closed Principle directly. At the National Bank of India, Pankaj once broke the card payment branch while adding net banking to a growing switch statement, which caused failed card payments in production. With Strategy, each payment method is its own class implementing PaymentStrategy, the checkout context just looks it up and delegates, and adding a wallet method is a new class registered in a map with zero edits to the context or the existing strategies.

In modern Java, a Strategy is often just a lambda because a single-method interface is a functional interface, so Rajesh can write an interest strategy as a one-line lambda. In Spring, injecting a Map gives runtime selection for free, so adding a payment method is literally adding a new bean.

Remember one sentence: Strategy makes algorithms interchangeable behind one interface, enforcing OCP because adding a new algorithm is a new class, never an edit.

---

Share
Use ← → arrow keys to navigate