Strategy: Pattern Implementation
The Problem It Solves
Checkout supports card, UPI, and wallet payments. The naive implementation is a switch inside CheckoutService — which grows with every mode, forces the whole service to re-test per addition, and cannot vary payment choice at runtime (per-user preference, feature flags). Strategy extracts each algorithm behind one interface; the context holds a strategy and delegates, blind to which one.
┌────────────────────┐
CheckoutService ─┤ context: holds ONE │ ── uses ──► PaymentStrategy ◁ interface
(stable code) │ PaymentStrategy │ + pay(Money): Result
└────────────────────┘ △
┌───────────┬───────────┬┴─────────┐
│ CardPay │ UpiPay │ WalletPay│
└───────────┴───────────┴──────────┘
swap the injected instance → behavior changes, zero edits here
Full Implementation
interface PaymentStrategy {
PaymentResult pay(Money amount);
}
class CardPayment implements PaymentStrategy {
private final CardGateway gateway;
@Override public PaymentResult pay(Money amount) { return gateway.charge(amount); }
}
class UpiPayment implements PaymentStrategy {
private final VpaResolver resolver;
@Override public PaymentResult pay(Money amount) { return resolver.collect(amount); }
}
class WalletPayment implements PaymentStrategy {
private final WalletStore wallets;
@Override public PaymentResult pay(Money amount) {
if (!wallets.hasBalance(amount)) throw new InsufficientFundsException();
return wallets.debit(amount);
}
}
// CONTEXT — closed forever:
class CheckoutService {
private final Map<PaymentType, PaymentStrategy> strategies;
CheckoutService(Map<PaymentType, PaymentStrategy> strategies) { this.strategies = strategies; }
PaymentResult checkout(Cart cart, PaymentType type) {
Money total = cart.total();
return strategies.get(type).pay(total); // ← the only variation point
}
}
New mode (BNPL) = new class + one registry entry. CheckoutService untouched since birth.
Runtime vs Construction-Time Selection
| Wiring | Behavior |
|---|---|
| Constructor injection | Strategy fixed for object lifetime (most common) |
| Setter/param injection | Switchable per transaction (e.g., user picks mode at runtime) |
| Registry map + request param | Both: strategies fixed set, selection dynamic |
Real-World Sightings
Collections.sort(list, Comparator)— sort is context, every comparator a strategy. The canonical JDK case.ThreadPoolExecutor.RejectedExecutionHandler— abort/caller-runs/discard policies.- Spring’s
AuthenticationProviderlist; servlet container connectionProtocolHandlers.
Trade-offs
| Gain | Cost |
|---|---|
| Open-closed growth on algorithms | Client must know strategy set |
| Each algorithm unit-testable in isolation | Indirection when reading flow |
| Kills conditional sprawl | More classes than an if-chain (until variant #3) |
| Runtime swappability | Stateful strategies need care under concurrency |
Beyond the Interface
The previous page wired strategies; this page covers the engineering decisions that make them pay off in real codebases: eliminating conditionals systematically, isolating algorithm tests, managing strategy state, and choosing between classes and lambdas.
Conditional Elimination — the Systematic Form
// BEFORE: every routing rule change edits shipping cost logic
Money shippingCost(Cart c) {
if (c.isPrime()) return Money.ZERO;
if (c.weight() > 10) return Money.of(199);
if (c.dest().isRemote()) return Money.of(299);
return Money.of(49);
}
// AFTER: rules become data + strategy objects
interface ShippingRule { Optional<Money> charge(Cart c); }
class PrimeFreeShipping implements ShippingRule {
public Optional<Money> charge(Cart c) {
return c.isPrime() ? Optional.of(Money.ZERO) : Optional.empty();
}
}
class WeightTier implements ShippingRule { ... } // etc.
Money shippingCost(Cart c) {
for (ShippingRule r : rulesInPriorityOrder) // order = business policy,
var m = r.charge(c); // configurable, not compiled
if (m.isPresent()) return m.get();
return defaultRate;
}
Rules become an ordered list — priority is configuration, not nested ifs. Adding a rule cannot disturb existing ones.
Testing Isolation
Each strategy tests alone with zero context scaffolding:
@Test void primeUsersShipFree() {
var cart = TestCart.prime().build();
assertEquals(Optional.of(Money.ZERO), new PrimeFreeShipping().charge(cart));
}
No mocking of checkout internals; the conditional-tangled original needed the full cart+user+destination fixture per case.
Strategy State & Concurrency
| Strategy kind | Concurrency stance |
|---|---|
| Stateless (pure functions of input) | Share freely; thread-safe by construction |
| Configured-immutable (holds injected deps, never mutates) | Share freely |
| Stateful (counters, caches inside) | Per-use instance or internal synchronization |
Default to stateless; document any deviation on the interface.
Class vs Lambda Decision
// trivial one-method strategy → lambda collapses ceremony:
strategies.put(PaymentType.UPI, amount -> upiResolver.collect(amount));
// multi-method contract or meaningful state → keep the class:
class FraudCheckingPayment implements PaymentStrategy { ... }
Rule of thumb: lambdas until a second method, injected dependency graph, or testability need forces a named type.
Failure Modes
- Parameter explosion: strategies whose
execute(ctx)takes a 12-field context map — the interface leaked; model proper parameter objects. - Hidden ordering dependencies: strategies assuming earlier ones ran — encode pipeline position explicitly.
- Selection logic regrowing elsewhere: a switch mapping enum→strategy belongs at composition root only.
The Problem It Solves
Ctrl+Z is a product requirement in editors, design tools, and admin consoles — and it’s hard precisely because undo needs to restore exact prior state including side effects. Command objects make undo tractable: every mutation is an object that can remember how to reverse itself, and history becomes a stack of those objects.
The Two Undo Strategies
STRATEGY A: INVERSE COMMANDS STRATEGY B: STATE SNAPSHOTS
execute() stores enough to run execute() captures full prior
the opposite operation later state (memento); undo restores it
AddText("hello") DocumentState(before)
undo → RemoveText("hello") undo ← swap state back
cheap per operation; inverse logic always correct; memory-heavy
must be written & correct per command for any object with snapshot()
| Inverse commands | Snapshots | |
|---|---|---|
| Memory per step | Small (diffs/params) | Full state size |
| Correctness risk | Inverse bugs accumulate | None if capture is complete |
| Best for | Structured edits (text ops, moves) | Small documents, config toggles |
Mechanics (inverse-command form)
interface Command {
void execute();
void undo();
}
class InsertTextCommand implements Command {
private final TextBuffer doc;
private final String text;
private final int at;
@Override public void execute() { doc.insert(at, text); }
@Override public void undo() { doc.delete(at, text.length()); }
}
class History {
private final Deque<Command> undoStack = new ArrayDeque<>();
private final Deque<Command> redoStack = new ArrayDeque<>();
void run(Command c) {
c.execute();
undoStack.push(c);
redoStack.clear(); // new edit invalidates the redo branch
}
void undo() {
var c = undoStack.poll();
if (c == null) return;
c.undo();
redoStack.push(c);
}
void redo() {
var c = redoStack.poll();
if (c == null) return;
c.execute(); // NOT undo() — re-run forward
undoStack.push(c);
}
}
Three invariants carry correctness: undo reverses in exact LIFO order; redo re-executes forward (never calls undo again — that would invert twice); any fresh edit clears the redo stack (the branching timeline is deliberately discarded).
Snapshot Form (Memento pairing)
class SetPriceCommand implements Command {
private final Product p; private final Money oldPrice, newPrice;
static SetPriceCommand of(Product p, Money np) {
return new SetPriceCommand(p, p.price(), np); // capture BEFORE mutating
}
public void execute(){ p.setPrice(newPrice); }
public void undo() { p.setPrice(oldPrice); }
}
Capture-before-mutate ordering is where undo bugs are born: capturing after the change records nothing useful.
Production Concerns
- Memory bound (illustrative math): 100-step history × 50 KB snapshots ≈ 5 MB — fine; 1 MB documents × unlimited steps is not. Cap history length or switch to diffs/coalescing.
- Coalescing: holding a key down generates thousands of insert commands — merge adjacent same-type commands into one undo unit.
- External side effects: emails sent, payments charged cannot be “un-done” by inverse execution — such operations need compensating actions (saga territory) or must be excluded from trivial undo.
- Multi-user editing: local stacks break under concurrent edits; OT/CRDT systems replace simple stacks.
Interview Framing
- Being handed “add undo to my editor” and producing the two-stack structure with the three invariants above is the expected full answer.
Premium Content
Unlock Strategy and all premium lessons with a subscription.
From ₹199.99/year — See plans