Back to Behavioral: Strategy, Observer, Command, State, Template
Q12Question 12 of 12

Design using the Strategy pattern

Key Concepts
  • The Strategy pattern defines a family of interchangeable algorithms behind a common interface, letting the client choose at runtime.
  • A context object holds a reference to one strategy and delegates to it, replacing conditional branching with polymorphism.
  • Adding a new algorithm means adding a new class with zero changes to the context, satisfying the Open/Closed Principle.
  • Java's Comparator is the most common built-in Strategy in everyday code.
  • Strategies can be injected at construction for fixed lifetime choices or passed per call for request-varying choices.
  • Each strategy is a small, focused class that is trivially unit-testable in isolation.
  • In banking, fee-calculation strategies let the system support flat, percentage, and waiver pricing without touching transfer logic.

Definition

The Strategy pattern makes an algorithm a pluggable object chosen at runtime behind a common interface.
  • Strategy interface – declares the single method that every algorithm must implement, such as computeFee(BigDecimal amount).
  • Concrete strategies – each class implements one algorithm variant: flat fee, percentage fee, premium waiver, and so on.
  • Context – holds a reference to a Strategy and delegates the work to it, never containing conditional logic that selects behaviour.
  • Runtime selection – the caller or a factory chooses which concrete strategy to inject, so the algorithm can change without recompiling the context.
  • Open/Closed Principle – a new algorithm is a new class, not an edit to existing code.

Think of the transfer counter at National Bank of India. The same transfer can be priced by a flat fee calculator, a percentage fee calculator, or a premium-customer waiver. Rajesh's transfer plugs in the percentage calculator while Priya's plugs in the flat one. The counter itself never changes; only the calculator in the slot changes. That slot is the strategy reference, and each calculator is a concrete strategy.

Understanding the Concepts

What is the Strategy pattern?

The Strategy pattern is a behavioural design pattern that defines a family of algorithms, encapsulates each one in its own class behind a common interface, and lets the client select which algorithm to use at runtime. The pattern replaces conditional logic that branches on behaviour with polymorphic objects.

In banking terms, the "algorithm" might be how a transfer fee is calculated. Instead of writing if (tier == PREMIUM) ... else if (tier == STANDARD) ... inside the transfer service, you define a FeeStrategy interface with a single method, and each pricing rule becomes a separate class.

  • The Strategy interface declares the algorithm contract.
  • Concrete strategies each implement one variant.
  • The Context holds a strategy reference and delegates to it.

Why does the Strategy pattern exist?

The pattern exists to eliminate conditional branching over behaviour. Without it, every new pricing rule means adding another else if branch inside the transfer service, which violates the Open/Closed Principle and makes the class grow without bound.

With Strategy, a new pricing rule is a new class. The transfer service never changes, which means existing, audited logic stays untouched. In a regulated banking environment at National Bank of India, this separation is critical because changing audited code triggers a full review cycle.

JAVA
// Without Strategy: fragile, growing conditional
public BigDecimal computeFee(BigDecimal amount, String tier) {
    if ("FLAT".equals(tier)) {
        return new BigDecimal("100");
    } else if ("PERCENTAGE".equals(tier)) {
        return amount.multiply(new BigDecimal("0.005"));
    } else if ("PREMIUM".equals(tier)) {
        return BigDecimal.ZERO;
    }
    throw new IllegalArgumentException("Unknown tier: " + tier);
}

Every new tier forces a change to this method. Strategy eliminates this problem entirely.

How does the Strategy pattern work internally?

The context object holds a field of type FeeStrategy (the interface). When the context needs to compute a fee, it calls strategy.computeFee(amount). The JVM dispatches the call to the concrete class that was injected, using standard polymorphism.

There is no reflection, no service locator, and no magic. It is plain interface-based polymorphism, which is why the pattern is both simple and powerful.

DIAGRAM
Client
  │
  │  chooses strategy
  ▼
Context ─── holds ──→ FeeStrategy (interface)
                          │
              ┌───────────┼───────────┐
              ▼           ▼           ▼
         FlatFee    PercentageFee  PremiumWaiver

How do you implement the Strategy pattern?

Here is a complete, runnable banking example where Pankaj (the architect) designs a fee-calculation strategy for the transfer service at National Bank of India.

JAVA
package com.bank.service.fee;

import java.math.BigDecimal;

public interface FeeStrategy {
    BigDecimal computeFee(BigDecimal transferAmount);
}
JAVA
package com.bank.service.fee;

import java.math.BigDecimal;

public class FlatFeeStrategy implements FeeStrategy {
    private final BigDecimal flatAmount;

    public FlatFeeStrategy(BigDecimal flatAmount) {
        this.flatAmount = flatAmount;
    }

    @Override
    public BigDecimal computeFee(BigDecimal transferAmount) {
        return flatAmount;
    }
}
JAVA
package com.bank.service.fee;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class PercentageFeeStrategy implements FeeStrategy {
    private final BigDecimal rate;

    public PercentageFeeStrategy(BigDecimal rate) {
        this.rate = rate;
    }

    @Override
    public BigDecimal computeFee(BigDecimal transferAmount) {
        return transferAmount.multiply(rate)
                .setScale(2, RoundingMode.HALF_UP);
    }
}
JAVA
package com.bank.service.fee;

import java.math.BigDecimal;

public class PremiumWaiverStrategy implements FeeStrategy {
    @Override
    public BigDecimal computeFee(BigDecimal transferAmount) {
        return BigDecimal.ZERO;
    }
}
JAVA
package com.bank.service;

import com.bank.service.fee.FeeStrategy;
import java.math.BigDecimal;

public class TransferService {
    private final FeeStrategy feeStrategy;

    public TransferService(FeeStrategy feeStrategy) {
        this.feeStrategy = feeStrategy;
    }

    public BigDecimal transfer(String fromAccount, String toAccount,
                               BigDecimal amount) {
        BigDecimal fee = feeStrategy.computeFee(amount);
        BigDecimal totalDebit = amount.add(fee);
        System.out.println("Transferring " + amount + " from " + fromAccount
                + " to " + toAccount + ", fee: " + fee
                + ", total debit: " + totalDebit);
        return fee;
    }
}
JAVA
package com.bank;

import com.bank.service.TransferService;
import com.bank.service.fee.*;
import java.math.BigDecimal;

public class StrategyDemo {
    public static void main(String[] args) {
        TransferService flatService =
                new TransferService(new FlatFeeStrategy(new BigDecimal("100")));
        flatService.transfer("RAJESH-001", "PRIYA-002",
                new BigDecimal("50000"));

        TransferService percentService =
                new TransferService(
                        new PercentageFeeStrategy(new BigDecimal("0.005")));
        percentService.transfer("RAJESH-001", "PRIYA-002",
                new BigDecimal("50000"));

        TransferService premiumService =
                new TransferService(new PremiumWaiverStrategy());
        premiumService.transfer("RAJESH-001", "PRIYA-002",
                new BigDecimal("50000"));
    }
}
TEXT
Transferring 50000 from RAJESH-001 to PRIYA-002, fee: 100, total debit: 50100
Transferring 50000 from RAJESH-001 to PRIYA-002, fee: 250.00, total debit: 50250.00
Transferring 50000 from RAJESH-001 to PRIYA-002, fee: 0, total debit: 50000

How is Strategy different from Template Method?

Both patterns let you vary behaviour, but they differ in mechanism and coupling.

| Aspect | Strategy | Template Method |
|---|---|---|
| Mechanism | Composition: algorithm is a separate object injected into the context | Inheritance: algorithm steps are overridden in subclasses |
| Coupling | Context depends on an interface, not a base class | Subclass is tightly coupled to the parent class |
| Swapping at runtime | Yes, inject a different strategy any time | No, the algorithm is fixed at construction |
| Number of classes | One context + N strategy classes | One abstract class + N subclasses |
| When to use | When behaviour varies independently of the object | When the skeleton is fixed but individual steps vary |

JAVA
// Strategy: behaviour injected
TransferService svc = new TransferService(new PercentageFeeStrategy(...));

// Template Method: behaviour inherited
public abstract class LoanProcessor {
    public final void process() {
        validate();
        calculateEMI();   // subclass fills this step
        disburse();
    }
    protected abstract void calculateEMI();
}

How is Strategy different from State?

Strategy and State look structurally identical, but their intent differs.

| Aspect | Strategy | State |
|---|---|---|
| Intent | Choose one algorithm from a family | Change behaviour as the object's internal state changes |
| Who decides | The client or a factory picks the strategy | The state objects themselves trigger transitions |
| Awareness of others | Strategies do not know about each other | Each state knows which states it can transition to |
| Lifetime | Typically fixed for the operation or object | Changes many times during the object's life |

JAVA
// Strategy: client picks once
TransferService svc = new TransferService(new FlatFeeStrategy(...));

// State: transitions happen internally
account.deposit(amount);  // state may change from FROZEN to ACTIVE

What are the rules and constraints?

The Strategy pattern has a few rules to follow:

  • The strategy interface must declare exactly the contract the context needs, nothing more.
  • All concrete strategies must be interchangeable through that interface; they must accept the same input and return the same output type.
  • The context must not contain any conditional logic that selects between behaviours. If it does, the pattern is not fully applied.
  • Strategies should be stateless when possible, which makes them thread-safe and reusable across contexts.

What are common mistakes?

The most common mistake is applying Strategy but still keeping conditional logic in the context.

JAVA
// Wrong: Strategy applied but conditional still present
public class TransferService {
    private final FeeStrategy strategy;

    public BigDecimal computeFee(BigDecimal amount, String tier) {
        if ("PREMIUM".equals(tier)) {
            return BigDecimal.ZERO; // bypasses strategy!
        }
        return strategy.computeFee(amount);
    }
}
JAVA
// Correct: all behaviour lives in the strategy
public class TransferService {
    private final FeeStrategy strategy;

    public BigDecimal computeFee(BigDecimal amount) {
        return strategy.computeFee(amount);
    }
}

Another mistake is creating a strategy for a single algorithm that will never vary. If there is only one pricing rule and no foreseeable second one, a plain method is simpler than a pattern.

When should you use the Strategy pattern?

Use Strategy when:

  • You have multiple algorithms for the same task and the choice varies by client, configuration, or request.
  • You want to add new algorithms without modifying existing code.
  • You want to unit-test each algorithm in isolation.
  • You want to eliminate growing if/else or switch blocks that branch on behaviour.

In banking at National Bank of India, Pankaj uses Strategy for fee calculation, interest computation, notification channels (SMS, email, push), and sorting account statements by date or amount.

When should you NOT use the Strategy pattern?

Do not use Strategy when:

  • There is only one algorithm and no realistic expectation of a second.
  • The algorithms share significant state or steps, in which case Template Method may be a better fit.
  • The overhead of extra classes is not justified for trivial logic.

What about lambdas and functional style?

Since Java 8, a strategy interface with a single method is a functional interface, so you can pass lambdas instead of creating named classes.

JAVA
package com.bank;

import com.bank.service.TransferService;
import java.math.BigDecimal;
import java.math.RoundingMode;

public class LambdaStrategyDemo {
    public static void main(String[] args) {
        // Lambda as strategy
        TransferService svc = new TransferService(
                amount -> amount.multiply(new BigDecimal("0.005"))
                        .setScale(2, RoundingMode.HALF_UP));
        svc.transfer("RAJESH-001", "PRIYA-002", new BigDecimal("100000"));
    }
}
TEXT
Transferring 100000 from RAJESH-001 to PRIYA-002, fee: 500.00, total debit: 100500.00

This is concise for simple algorithms. For complex strategies with multiple methods or shared state, named classes remain clearer.

What about thread safety?

Stateless strategies are inherently thread-safe because they hold no mutable state. If a strategy must hold configuration like a rate, make the field final and set it in the constructor, so the strategy is effectively immutable.

JAVA
public class PercentageFeeStrategy implements FeeStrategy {
    private final BigDecimal rate; // immutable, thread-safe

    public PercentageFeeStrategy(BigDecimal rate) {
        this.rate = rate;
    }
}

If you need a strategy that accumulates state across calls, synchronize access or use a thread-local strategy instance.

What should a senior engineer know?

A senior engineer should know:

  • Construction-time vs per-call injection. Inject the strategy at construction when the choice is fixed for the object's lifetime. Pass it as a method parameter when the choice varies per request, such as selecting a notification channel.
  • Comparator is Strategy. Java's Comparator is the most widely used Strategy in the JDK: list.sort(Comparator.comparing(Account::getBalance)).
  • Spring integration. In Spring, define each strategy as a @Component and inject a Map or a List to select at runtime by name or predicate.
  • Strategy + Factory. Combine Strategy with a Factory that maps a customer tier to the right strategy, so the caller never manually picks.

What are common interview follow-up questions?

  • How does Strategy differ from Template Method?
  • How does Strategy differ from State?
  • Can you use lambdas instead of strategy classes?
  • How would you pick the right strategy at runtime in a Spring application?
  • Give a real-world example of Strategy in the JDK.

Visual Overview

DIAGRAM
  ┌─────────────────────────────────────────────────┐
  │              TransferService (Context)           │
  │                                                  │
  │  - feeStrategy : FeeStrategy                     │
  │  + transfer(from, to, amount) : BigDecimal       │
  │       │                                          │
  │       │  delegates                               │
  │       ▼                                          │
  │  feeStrategy.computeFee(amount)                  │
  └─────────────────────────────────────────────────┘
                        │
            ┌───────────┼───────────────┐
            ▼           ▼               ▼
      ┌──────────┐ ┌──────────────┐ ┌──────────────┐
      │ FlatFee  │ │PercentageFee │ │PremiumWaiver │
      │ Strategy │ │  Strategy    │ │  Strategy    │
      ├──────────┤ ├──────────────┤ ├──────────────┤
      │return 100│ │return amt*0.5│ │return 0      │
      └──────────┘ └──────────────┘ └──────────────┘
DIAGRAM
  Without Strategy           With Strategy
  ───────────────           ──────────────
  if (flat)                 context.computeFee()
    return 100                  │
  else if (pct)                 ▼
    return amt * 0.5%       strategy.computeFee()
  else if (premium)             (polymorphic dispatch)
    return 0
  else if (...)             New rule = new class
    // grows forever        Zero change to context

Key Takeaway

The Strategy pattern turns an algorithm into a pluggable object chosen at runtime behind a common interface. Whenever you find yourself writing a branching statement that selects between different behaviours, such as different pricing rules, different sorting orders, or different notification channels, that is the signal to replace the conditional with a strategy interface and a set of small, focused implementation classes.

The structure is a strategy interface declaring the operation, several concrete strategies each implementing one variant, and a context that holds a reference to one strategy and simply delegates to it. The crucial point is that the context contains no conditional logic over behaviour. It just calls the strategy it was given. This directly satisfies the Open/Closed Principle, because a new pricing rule is a new class with zero change to the context, and Dependency Inversion, because the context depends only on the interface.

A major practical advantage is testability. Each strategy is a tiny isolated unit, so Rajesh can test the percentage fee calculation on its own without standing up the whole transfer flow at National Bank of India. Since Java 8, single-method strategy interfaces are functional interfaces, so simple algorithms can be expressed as lambdas, while complex ones remain named classes.

The design decision to weigh is whether to inject the strategy once at construction, which is clean when the choice is fixed for the object's lifetime, or to pass it per call, which suits cases like choosing a notification channel that varies request by request. In banking, pricing transfers by customer tier through fee strategies is exactly what lets the team introduce a new fee rule as a single new class without ever touching the already-audited pricing logic.

Remember one sentence: the Strategy pattern replaces conditional branching over behaviour with a pluggable interface, so a new algorithm is a new class and the context never changes.

---

Share
Use ← → arrow keys to navigate