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:
- Recognise when a fixed sequence of steps, repeated for several variants, is at risk of being written slightly differently each time
- Write an abstract class that seals the sequence with
finaland exposes hook methods for the parts that actually differ - Choose between an abstract class and a
defaultinterface method for the same idea, based on whether the sequence needs to carry state - Say when one fixed order with no real variant is not worth this shape at all
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 →