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, record what happened for the customer.

So write 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

Four stages, all following the same three steps. Easy to read, and you can check it against the requirement without trouble.

3. A new requirement

Cancellation arrives, and it does not fit the pattern the other four share.

It is only legal while an order is still PLACED or ACCEPTED. And it 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 a reviewer can still read it top to bottom.

But look carefully at what just happened. The fixed idea of "check, apply, record" got written out by hand a second time, with its own legality rule and its own 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: check legality first, apply the change second, record it third, in that order — inside a method 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 with two other variants tangled through it.

Picture the failure concretely. A stage that records itself before checking whether the move was legal. It compiles. It looks almost right.

5. The move

Fix the three-step sequence once, in a method nothing can override. Then 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.

Notice PlainStage needed no new class at all. Just a constructor call, because a stage with nothing special about it is plain data.

That is Template Method. One method owns the order steps run in, and each stage fills in only its own two blanks.

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. Reach for it when a class already extends something else and has no spare slot in its hierarchy for OrderStage.

Your tie-breaker between the two has nothing to do with style. It is whether the sequence needs to carry state from one step to the next. An interface cannot do that. It declares methods, and it cannot declare an instance field the way OrderStage could if a later stage needed one.

The other real difference will bite you. A default method cannot be declared final. Nothing stops an implementer overriding the whole sequence by accident and skipping every hook it was supposed to call.

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. Overriding approve directly is completely legal for a default method.

For ApprovalWorkflow that is acceptable. It needs no field, since both hooks only look at the Request they are handed, so an interface is the right shape.

OrderStage's sequence was worth sealing with final for a specific reason: skipping the legality check is exactly the 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.

Write the three steps inline once, the way section 2 does. It costs nothing extra and stays easier to read end to end when there is only one path through it.

Your 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. Build the hook machinery for it anyway and this app's grading standard scores it over-engineered (premature interface) — the same tag every earlier chapter's speculative interface 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 trick SkipsChecks used, but against OrderStage, 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

That error is the whole reason to prefer an abstract class over a default method whenever you have the choice.

final on a method is only legal in a class. Never on an interface method, default or otherwise.

When the fixed sequence protects an invariant that actually matters — the way "check before you charge" does for OrderStage — that difference stops being a style preference. It decides whether the compiler catches your mistake or your users do.

Why this matters in an interview

Ask an interviewer for a fifth or sixth variant on a working design and they are watching 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 saying which two things actually vary between your stages, and 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 →