Chain of responsibility and template method
Part of the free Low-Level Design course on LogicWiz, in the chapter: Behavioural patterns II. The class-diagram exercises run in the app.
Week 8: The validate method that ate the sprint
"This validate method is 120 lines long. Where do I add the prescription check for medicine orders?"
Arjun, asking Priya before a deadline
One method, many checks
Before SwiftKart accepts an order, it runs checks. Is every item in stock? Is the delivery slot still free? Is the address inside the delivery zone? Is the payment method valid? Here is a short version of Arjun's code:
public class OrderValidator {
public String validate(Order order) {
for (Item item : order.items()) {
if (stock.available(item.sku()) < item.qty()) {
return "Out of stock: " + item.sku();
}
}
if (!slots.isFree(order.slotId())) {
return "Slot is full";
}
if (!zones.covers(order.address())) {
return "We do not deliver here yet";
}
if (order.payment() == null) {
return "Choose a payment method";
}
return null;
}
}
Now three requests arrive:
- Medicine orders need a prescription check.
- Orders from business customers skip the slot check.
- The cheap checks should run before the slow ones, so the order of checks matters.
Every request edits this one method. The checks cannot be reused or tested alone.
Pass the order along a chain
The problem: a request goes through several independent steps. Any step can stop it. The list of steps and their order change by context.
The fix: make each check a small object called a handler. Each handler does its one check. If the check passes, it hands the order to the next handler. If it fails, it stops the chain and returns the error.
public abstract class ValidationHandler {
private ValidationHandler next;
public ValidationHandler setNext(ValidationHandler next) {
this.next = next;
return next;
}
public String handle(Order order) {
String error = check(order);
if (error != null || next == null) {
return error;
}
return next.handle(order);
}
protected abstract String check(Order order);
}
A concrete handler fills in only check:
public class SlotCheck extends ValidationHandler {
private final SlotService slots;
public SlotCheck(SlotService slots) { this.slots = slots; }
protected String check(Order order) {
return slots.isFree(order.slotId()) ? null : "Slot is full";
}
}
The checkout builds the chain once, in the order it wants. Because setNext returns the next handler, the calls read left to right:
ValidationHandler chain = new StockCheck(stock);
chain.setNext(new SlotCheck(slots))
.setNext(new ZoneCheck(zones))
.setNext(new PaymentCheck());
String error = chain.handle(order);
flowchart TD
O["Order"] --> A["StockCheck"]
A -->|"pass"| B["SlotCheck"]
A -->|"fail"| X["Return error"]
B -->|"pass"| C["ZoneCheck"]
B -->|"fail"| X
C -->|"pass"| D["PaymentCheck"]
C -->|"fail"| X
D -->|"pass"| OK["Order accepted"]
D -->|"fail"| X
This is the Chain of Responsibility pattern. The sender gives a request to the first handler and does not know which handler will act on it. A medicine order gets a chain with a PrescriptionCheck added. A business order gets a chain without SlotCheck. No existing handler changes.
There are two flavours. In the classic one, exactly one handler deals with the request. For example, a refund request goes to an agent, then a team lead, then a manager, depending on the amount. In the validation flavour, every handler runs until one fails. Both are chains.
💡 Tip: Many teams write a simpler version: a
List<OrderCheck>and aforloop that stops at the first error. That is fine and easier to read. Use the linked chain when a handler must decide whether the next one runs at all.
Order inside the chain matters. Put cheap checks first and slow ones last. Checking the address zone is a lookup in memory; checking stock may call another service. So a better chain starts with ZoneCheck and puts StockCheck after it. If the cheap check fails, the slow one never runs. Also put any check that reserves something, such as holding a slot, at the end. Otherwise a later failure leaves a reservation behind that someone must undo.
When not to use a chain: two or three checks that never change are clearer as plain method calls in one place.
Same steps, different details
A second problem. Every Monday, SwiftKart pays riders and dark stores (the small warehouses that pack orders). Both payouts follow the same steps:
- Load last week's entries.
- Work out the amount.
- Deduct TDS, the tax deducted at source.
- Send the money to the bank.
- Send a statement.
Only steps 1 and 2 differ. Riders earn per delivery; stores earn a share of sales. Arjun had two classes with the other three steps copied. Last month someone fixed the TDS rounding in one copy and not in the other.
The fix: write the fixed order of steps once, in a base class. Let subclasses fill in only the steps that vary.
public abstract class PayoutJob {
public final void run(LocalDate week) {
List<Entry> entries = load(week);
int gross = calculate(entries);
int net = gross - tds(gross);
transfer(net);
sendStatement(net);
}
protected abstract List<Entry> load(LocalDate week);
protected abstract int calculate(List<Entry> entries);
private int tds(int gross) { return gross / 100; }
private void transfer(int amount) { /* bank call */ }
protected void sendStatement(int amount) { /* email by default */ }
}
public class RiderPayout extends PayoutJob {
private final DeliveryLog deliveries = new DeliveryLog();
protected List<Entry> load(LocalDate week) { return deliveries.forWeek(week); }
protected int calculate(List<Entry> entries) { return entries.size() * 30; }
}
This is the Template Method pattern. run is the template method: it fixes the order of steps. It is final, so no subclass can reorder or skip a step. The abstract methods are the steps a subclass must fill. sendStatement has a default body; a step like that is called a hook, and a subclass may override it. The chain's handle method above uses the same idea in a small way.
Template method or strategy?
In a mock interview, a candidate said: "Template method and strategy are the same. Both let you change one step." Both do let you vary behaviour. They do it in different ways:
- Template method uses inheritance. The variation is a subclass. It is fixed when the object is created, and there is one axis of variation.
- Strategy uses composition. The variation is an object held in a field. You can swap it at runtime, and you can hold several strategies for different steps.
Watch what happens when a second step varies. Say statements can go by email or WhatsApp. With inheritance you need RiderEmailPayout, RiderWhatsAppPayout, StoreEmailPayout and so on. The classes multiply. At that point, keep the template for the fixed skeleton and pass a StatementSender strategy into it.
Use template method when the skeleton is fixed and only one family of steps varies. Do not use it to share code between classes that are not really the same kind of thing; that is the fragile base class problem from Week 2.
Practice: a chain and a template
First, split the long validator into a chain. Make an abstract handler with a link to the next one, and at least three checks.
Then remove the copied payout steps. Add an abstract payout class with a template method and the steps that vary.
📌 Summary: A chain passes a request through small handlers, each able to stop it. Template method fixes the order of steps in a base class. It uses inheritance; strategy uses composition and can change at runtime.
What Arjun learns next
Arjun now has many small classes that call each other. Priya asks him to show, in order, who calls whom when a customer taps Pay. Next week: sequence diagrams.
Check your understanding
- Medicine orders need an extra prescription check. With the validation chain in place, what changes?
- A candidate says: "Template method and strategy are the same, both change one step." What is the main difference?
- Why is PayoutJob.run() marked final?
- Payouts now vary in two ways: how the amount is worked out, and whether the statement goes by email or WhatsApp. What is the best design?