The Problem It Solves
Payment modes arrive monthly: card today, UPI next sprint, wallets after that. If payment logic sits inline inside checkout code, each addition edits battle-tested flow — every edit risks the whole checkout, every addition grows the diff review surface. The pattern across nearly all maintenance pain is identical: stable logic and unstable logic live in the same place, so change in one damages the other.
The principle: identify what varies, isolate it behind a stable interface, and let the stable side depend only on that interface.
BEFORE — variation embedded AFTER — variation sealed
┌───────────────────────────┐ ┌────────────────┐
│ Checkout │ │ Checkout │
│ if card → chargeCard() │ extract │ + │
│ if upi → collectVpa() │ ────────► │ depends on │
│ if wallet→ debitWallet() │ │ PaymentMode │
│ (edited every release) │ │ (never edited) │
└───────────────────────────┘ └───────┬────────┘
▼
CardMode | UpiMode | WalletMode ...
(new classes, old code untouched)
Left: one class absorbing every market change. Right: the axis of change became a family of plug-ins; Checkout hasn’t been modified since the extraction — new payment modes are additions.
How to Find What Varies
- Listen for “and” / “or” in requirements (“supports cards or UPI or wallets”) — conjunctions of alternatives mark an axis.
- Listen for future tense: “later we’ll add…”, “for now only X” — stated future variation is free design input.
- Watch churn history: any file edited weekly by different people contains a hidden variation axis.
- Ask the direct question in interviews: “What is most likely to change here?” The answer names the seam.
Mechanics
interface PaymentMode { PaymentResult pay(Money amt); }
class Checkout {
private final Map<PaymentType, PaymentMode> modes; // injected
PaymentResult pay(PaymentRequest req) {
validateCart(req.cart()); // stable part
return modes.get(req.type()).pay(req.amount()); // varying part
}
}
The stable method’s structure never changes; the map absorbs growth.
What It Does Not Solve
- It does not predict the future — it localizes known or stated variation. Guessing axes nobody asked for produces speculative generality.
- It does not remove conditional complexity from genuinely fixed logic; extracting a single never-changing branch adds indirection for nothing.
Trade-offs
| Inline variation | Encapsulated variation | |
|---|---|---|
| First implementation | Faster | Slightly slower (extra interface) |
| Each later variant | Edit core, retest everything | Add class only |
| Blast radius of a bad variant | Whole flow | That variant alone |
| Risk if axis guessed wrong | Low (nothing built) | Wasted abstraction |
Interview Signals
- Answering “what varies?” before drawing any diagram separates designers from transcribers — it is the key that selects which pattern (Strategy for algorithms, Factory for creation, Observer for notification) applies.
- Stating the cost honestly — “I’m adding one interface to buy isolation on this axis” — reads as judgment, not ceremony.
Premium Content
Unlock Encapsulate What Varies and all premium lessons with a subscription.
From ₹199.99/year — See plans