LLD Dojo

Patterns you will actually be asked for · chapter 25 of 33

Template method: a fixed skeleton with holes

Chapter 3.7 · Part 3, Patterns you will actually be asked for · about 25 minutes

What you need before this chapter: Part 1 in full, especially abstract classes (1.5). Part 2 in full, especially open/closed (2.5) and Liskov substitution (2.6). Chapter 3.1, Strategy, for comparison: that chapter swaps one decision, and this one owns a sequence of several.

When you finish this chapter you will be able to:


1. The situation

An order moves through four stages: placed, accepted, preparing, ready. Moving to any stage does the same three things: check the move is legal, apply it, and record what happened for the customer. The obvious way to write this is one method that does all three for every stage.

static void advance(Order order, OrderState next) {
    if (order.state.ordinal() + 1 != next.ordinal()) {
        throw new IllegalStateException("cannot move from " + order.state + " to " + next);
    }
    order.state = next;
    order.history.add("moved to " + next + ", notified customer");
}

2. Naive code that is fine

javac Step1.java
java Step1
moved to ACCEPTED, notified customer
moved to PREPARING, notified customer
moved to READY, notified customer

For four stages that all follow the exact same three steps, this is easy to read, and a reviewer can check it against the requirement without any trouble.

3. A new requirement

Cancellation arrives, and it does not fit the pattern the other four stages share. It is only legal while an order is still PLACED or ACCEPTED, and it also has to zero the charge, which no other stage does. The obvious response is a special case, checked first.

static void advance(Order order, OrderState next) {
    if (next == OrderState.CANCELLED) {
        if (order.state != OrderState.PLACED && order.state != OrderState.ACCEPTED) {
            throw new IllegalStateException("too late to cancel from " + order.state);
        }
        order.state = OrderState.CANCELLED;
        order.chargeCents = 0;
        order.history.add("cancelled, refunded, notified customer");
        return;
    }

    if (order.state.ordinal() + 1 != next.ordinal()) {
        throw new IllegalStateException("cannot move from " + order.state + " to " + next);
    }
    order.state = next;
    order.history.add("moved to " + next + ", notified customer");
}
javac Step2.java
java Step2
charge: 0
moved to ACCEPTED, notified customer
cancelled, refunded, notified customer

This works, and it is still a method a reviewer can read top to bottom. It is worth noticing exactly what happened, though: the fixed idea of "check, apply, record" got repeated by hand a second time, with its own separate legality rule and its own separate recording line, right next to the first copy.

4. Watch where it goes, and the real cost

A sixth stage will need the same three steps written out a third time. Whoever adds it has to remember, from scratch, to check legality first, apply the change second, and record it third, in that order, inside a method that is growing a new special case for every stage that needs one. The three steps are not hard to write once. They are easy to get subtly wrong the third time, under deadline pressure, in the middle of a method that already has two other variants tangled through it. A stage that records itself before actually checking whether the move was legal would compile and would look almost right.

5. The move

Fix the three-step sequence once, in a method nothing can override, and let each stage plug in only the two things that actually differ: how it checks legality, and what extra happens once it has been entered.

abstract class OrderStage {
    private final OrderState state;

    OrderStage(OrderState state) {
        this.state = state;
    }

    // The fixed sequence. No subclass can reorder, skip, or wrap these three steps.
    final void enter(Order order) {
        check(order);
        order.state = state;
        String note = onEntered(order);
        order.history.add("moved to " + state + (note.isEmpty() ? "" : ", " + note) + ", notified customer");
    }

    void check(Order order) { }

    String onEntered(Order order) { return ""; }
}

A stage with nothing special to do overrides neither hook.

final class PlainStage extends OrderStage {
    PlainStage(OrderState state) { super(state); }
}

CancelledStage overrides both.

final class CancelledStage extends OrderStage {
    CancelledStage() { super(OrderState.CANCELLED); }

    @Override
    void check(Order order) {
        if (order.state != OrderState.PLACED && order.state != OrderState.ACCEPTED) {
            throw new IllegalStateException("too late to cancel from " + order.state);
        }
    }

    @Override
    String onEntered(Order order) {
        order.chargeCents = 0;
        return "refunded";
    }
}
javac Step3.java
java Step3
charge: 0
moved to ACCEPTED, notified customer
moved to CANCELLED, refunded, notified customer

enter is final, so check, then the move, then onEntered, then the recording line, happen in that order, every time, for every stage that will ever exist. PlainStage needed no new class at all, just a constructor call, because a stage with nothing special about it is data, not behaviour. This is Template Method: one method owns the order steps run in, and each stage only fills in the two blanks that are actually its own. corpus/food-ordering's own OrderStage and CancelledStage are this same shape, five fixed steps and the same two hooks, on a six-stage order lifecycle.

6. What modern Java changes here

An interface with a default method can carry a fixed sequence too, and it is worth reaching for when a class already extends something else and has no spare slot in its hierarchy for OrderStage. The tie-breaker between the two is not style. It is whether the sequence needs to carry state from one step to the next. An interface cannot answer yes to that: it can declare methods, but it cannot declare an instance field the way OrderStage could if a later stage needed one.

The other real difference is that a default method cannot be declared final. Nothing stops an implementer from overriding the whole sequence by accident, skipping every hook it was supposed to call through.

interface ApprovalWorkflow {
    default boolean approve(Request request) {
        if (!checkFormat(request)) return false;
        if (!checkLimit(request)) return false;
        return true;
    }

    boolean checkFormat(Request request);
    boolean checkLimit(Request request);
}
static final class SkipsChecks implements ApprovalWorkflow {
    @Override
    public boolean approve(Request request) { return true; }

    @Override
    public boolean checkFormat(Request request) { return false; }

    @Override
    public boolean checkLimit(Request request) { return false; }
}
javac Step5.java
java Step5
standard.approve(tooBig): false
skips.approve(tooBig): true

SkipsChecks compiles cleanly and approves a request that both of its own hooks would reject, because overriding approve directly is completely legal for a default method. ApprovalWorkflow needs no field here, since both hooks only ever look at the Request they are handed, so an interface is the right shape. OrderStage's sequence, by contrast, was worth sealing with final specifically because skipping the legality check was the exact mistake section 4 was trying to prevent.

7. When naming it is wrong

A single order lifecycle with no second variant ever planned does not need enter split into hooks. Writing the three steps inline once, the way section 2 does, costs nothing extra, and it stays easier to read end to end when there is only one path through it.

The threshold: reach for this once at least two variants genuinely share one fixed sequence, and only a couple of steps differ between them. One order type, with CANCELLED never on the roadmap, does not meet that bar, and building the hook machinery for it scores as `over-engineered (premature interface)` under this app's grading standard, the same tag every earlier chapter's speculative interface has earned.

Your turn

Add a READY stage that also sends an SMS to the customer, using onEntered, with no change to OrderStage.

The answer.

final class ReadyForPickupStage extends OrderStage {
    ReadyForPickupStage() { super(OrderState.READY); }

    @Override
    String onEntered(Order order) {
        return "SMS sent";
    }
}
javac Step4.java
java Step4
moved to ACCEPTED, notified customer
moved to PREPARING, notified customer
moved to READY, SMS sent, notified customer

One new class, one overridden hook. OrderStage.enter ran the same three steps it always does.

Going deeper

Try the same trick SkipsChecks used against OrderStage itself, where enter is final.

static final class BadStage extends OrderStage {
    BadStage() { super(OrderState.ACCEPTED); }

    @Override
    void enter(Order order) {
        // skip the check entirely
    }
}
javac Step6Bad.java
Step6Bad.java:27: error: enter(Order) in BadStage cannot override enter(Order) in OrderStage
        void enter(Order order) {
             ^
  overridden method is final
1 error

This is the whole reason to prefer an abstract class over a default method whenever the choice is open. final on a method is only legal in a class, never on an interface method, default or otherwise. When the fixed sequence is protecting an invariant that actually matters, the way "check before you charge" does for OrderStage, that difference is not a style preference. It is the difference between a mistake the compiler catches and a mistake that ships.

Why this matters in an interview

An interviewer who asks you to add a fifth or sixth variant to a working design is watching for one thing. Do you edit the same lines a third time, or do you notice that three steps keep repeating and give them one home? Naming Template Method matters less than being able to say which two things actually vary between your stages, and being able to explain why you sealed the rest with final instead of leaving it open to a mistake like BadStage's.


Next: chapter 3.8, Adapter and Facade: making other people's code fit. This chapter fixed the order steps run in in your own code. The next one deals with code you do not own, and cannot change, that does not fit the shape your code needs.

← 3.6 Decorator and Chain of Responsibility · All chapters · 3.8 Adapter and Facade: making other people's code fit →