LLD Dojo

Template Method

Core. Expect to meet this one, and expect to be asked for it by name.

Start with the problem

An order moves through six stages: placed, accepted, preparing, ready, out for delivery, delivered. At every stage the same five things need to happen: check that the move is legal, apply it, timestamp it, let that stage do its own work, then notify whoever cares. The obvious way to write this is one method that handles all six stages by hand.

StageEvent advance(Order order, OrderState next, Clock clock, Notifier notifier) {
    if (!order.state().canMoveTo(next)) {
        throw new IllegalStateException("cannot move to " + next);
    }
    Instant at = clock.instant();
    order.moveTo(next);
    // stage-specific work would go here, one branch per stage
    List<Audience> told = notifyAudiencesFor(next, order, notifier);
    StageEvent event = new StageEvent(next, at, told);
    order.record(event);
    return event;
}

For six stages that mostly look alike, this reads fine, and every step is visible in one place.

Watch where it goes

A requirement arrives for the CANCELLED stage: cancellation is only legal while the order is still PLACED or ACCEPTED, and a cancelled order is charged nothing. That is one more branch inside advance, checking a state set before doing anything, and one more branch after the move, zeroing the charge.

The five steps are not the hard part to write once. The hard part is writing them correctly a second time and a third. Whoever adds the next stage after CANCELLED has to remember to read the clock, apply the move, run that stage's own work, and notify, in that exact order, inside the same sprawling method. A stage written under deadline pressure is exactly where somebody skips the timestamp, or notifies before the move is actually applied.

The move

Fix the sequence once, in a method no subclass can override, and let each stage fill in only the two steps that actually differ.

public abstract class OrderStage {

    private final OrderState state;
    private final List<Audience> audiences;

    protected OrderStage(OrderState state, List<Audience> audiences) {
        this.state = state;
        this.audiences = List.copyOf(audiences);
    }

    public final StageEvent enter(Order order, Clock clock, Notifier notifier) {
        Instant at = clock.instant();
        check(order, at);
        order.moveTo(state);
        onEntered(order, at);
        StageEvent event = new StageEvent(state, at, tell(order, notifier));
        order.record(event);
        return event;
    }

    protected void check(Order order, Instant at) { }

    protected void onEntered(Order order, Instant at) { }

    private List<Audience> tell(Order order, Notifier notifier) {
        List<Audience> told = new ArrayList<>();
        for (Audience a : audiences) {
            try {
                notifier.send(a, order.id(), state);
                told.add(a);
            } catch (RuntimeException down) { /* one channel down costs one notification */ }
        }
        return told;
    }
}

This is corpus/food-ordering's OrderStage. enter is final, so no stage can reorder, skip, or wrap the three fixed steps. CancelledStage overrides both hooks: check refuses once the order has passed ACCEPTED, and onEntered charges nothing. Three of the six stages in this corpus, including PLACED and PREPARING, override neither hook. Each of those is new PlainStage(state, audiences), a single constructor call, because a stage with no behavior of its own is data, not a class.

What modern Java changes here

An interface with default methods can carry a fixed sequence too, when a class already extends something else and has no superclass slot to spare for OrderStage. The cost is that a default method cannot be declared final, so an implementer could override the whole sequence by accident rather than only the hooks it is supposed to fill in. An abstract class that seals the sequence with final is the safer choice whenever nothing else forces the hierarchy to be an interface.

The real competitor to check against first is a policy object, not the interface-versus-class question. Tariff, in the same corpus, is chosen by its caller, answers one question, and has no idea when it runs or what else is going on: swapping one changes an answer. OrderStage owns the sequence and calls down into a subclass at points it alone decides: overriding a hook changes a step, not the order steps happen in. Reach for Template Method only once the sequence itself, not one answer inside it, is the thing worth fixing in one place.

When naming it is wrong

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

The threshold: reach for this once several variants genuinely share one fixed sequence and only a couple of steps differ between them. corpus/food-ordering's six stages meet that bar. A system with one order type and nothing else in view does not, and building the hook machinery for it scores as over-engineered (premature interface) under the Standard's D3 dimension.

Where this lives in the app

Syllabus item B8 measures a refusal path written by hand, inside OrderBook.reject(), against the stage version. The hand-written path costs 25 lines because it repeats all five steps a fifth time. The stage version costs 7 lines against a budget of 11, and OrderStage itself is never touched by either change, as DECISION_LOG.md records.

All reference pages