Definition
- 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
Strategyand 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.
// 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.
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.
package com.bank.service.fee;
import java.math.BigDecimal;
public interface FeeStrategy {
BigDecimal computeFee(BigDecimal transferAmount);
}
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;
}
}
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);
}
}
package com.bank.service.fee;
import java.math.BigDecimal;
public class PremiumWaiverStrategy implements FeeStrategy {
@Override
public BigDecimal computeFee(BigDecimal transferAmount) {
return BigDecimal.ZERO;
}
}
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;
}
}
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"));
}
}
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 |
// 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 |
// 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.
// 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);
}
}
// 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/elseorswitchblocks 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.
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"));
}
}
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.
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
Comparatoris the most widely used Strategy in the JDK:list.sort(Comparator.comparing(Account::getBalance)). - Spring integration. In Spring, define each strategy as a
@Componentand inject aMapor aListto 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
┌─────────────────────────────────────────────────┐
│ 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 │
└──────────┘ └──────────────┘ └──────────────┘
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.
---