Template method
A template method fixes the steps of a procedure and their order in one base-class method, and lets subclasses fill in the steps that differ. This page turns Pedal's copied city reports into one template, so a VAT fix lands in every city at once.
1. The pain
Each city Pedal runs in wants a monthly report: number of rides, revenue, VAT, sent to the city council. London's report was written first. Paris's was a copy of London's with the title translated, euros instead of pounds, an extra line about e-bikes, and a different address.
Then finance found that VAT was being worked out on the wrong base and fixed London's copy. Paris's copy kept the bug for three months. The steps and their order are the same for every city; only some of the steps differ. The copies hid that.
2. The idea in one picture
Template method puts the fixed sequence of steps in one base-class method (the template method, here run()) that calls other methods for the steps. Some steps are written once in the base (collect(), the VAT rule); some must be filled in by each subclass (title(), summary(), deliver()); some are hooks that subclasses may fill in if they want (extra(), empty by default).
The base class calls the subclass, not the other way round, which is sometimes called the Hollywood principle: "don't call us, we'll call you". In Java the template method is often marked final so subclasses can't reorder the steps.
3. The roles
| Role | Its job | In Pedal |
|---|---|---|
| Abstract class | Holds the template method and the shared steps. | MonthlyReport with run(), collect(), vat() |
| Template method | Calls the steps in a fixed order. | run(month) |
| Primitive operations | Steps every subclass must fill in. | title(), summary(), deliver() |
| Hook | An optional step with a default. | extra(), empty unless a city adds lines |
| Concrete class | Fills in the steps for one case. | LondonReport, ParisReport |
4. Before and after
The city reports both ways (Python sketch; the full program is in section 5).
Without the pattern
def london_report(month):
rides, gross = load("london")
vat = gross // 6 # fixed here
lines = [f"== London, {month} =="]
lines.append(f"rides {rides} ...")
send("reports@london...", lines)
def paris_report(month): # a copy
rides, gross = load("paris")
vat = gross * 20 // 100 # still wrong
...With it
class MonthlyReport(ABC):
def run(self, month):
rides, gross = self.collect()
lines = [self.title(month)]
lines.append(self.summary(rides, gross))
lines += self.extra() # hook
lines.append(self.deliver())
return lines
class ParisReport(MonthlyReport):
def title(self, month): ...
def extra(self):
return ["vélos électriques : 41 %"]| The next change | Without the pattern | With it |
|---|---|---|
| Fix the VAT rule | Fix every city's copy, and hope none is missed | Fix vat() once in the base |
| Add Berlin | Copy a whole report again | A BerlinReport with three short methods |
| Every report must be archived after sending | Edit every copy | One line in run() |
The order lives in one place, and each subclass contains only what makes its city different.
5. The code
MonthlyReport.run() fixes the order; London and Paris fill in the title, summary and delivery, and Paris also uses the optional extra() hook.
from __future__ import annotations
from abc import ABC, abstractmethod
RIDES = {"london": (61_200, 4_896_000), "paris": (22_400, 1_792_000)} # rides, revenue in pence or cents (VAT included)
class MonthlyReport(ABC):
"""The template method run() fixes the steps and their order; subclasses fill some of them in."""
city = ""
def run(self, month: str) -> list[str]:
rides, gross = self.collect() # shared step
lines = [self.title(month)] # must be filled in
lines.append(self.summary(rides, gross)) # must be filled in
lines += self.extra() # hook: optional, empty by default
lines.append(self.deliver()) # must be filled in
return lines
def collect(self) -> tuple[int, int]:
return RIDES[self.city]
@staticmethod
def vat(gross: int) -> int:
return gross // 6 # 20% VAT included in the price
@abstractmethod
def title(self, month: str) -> str: ...
@abstractmethod
def summary(self, rides: int, gross: int) -> str: ...
def extra(self) -> list[str]:
return []
@abstractmethod
def deliver(self) -> str: ...
class LondonReport(MonthlyReport):
city = "london"
def title(self, month: str) -> str:
return f"== London, {month} =="
def summary(self, rides: int, gross: int) -> str:
return f"rides {rides} | revenue £{gross // 100} | VAT £{self.vat(gross) // 100}"
def deliver(self) -> str:
return "sent to reports@london-council.example"
class ParisReport(MonthlyReport):
city = "paris"
MONTHS = {"October": "octobre"}
def title(self, month: str) -> str:
return f"== Paris, {self.MONTHS[month]} =="
def summary(self, rides: int, gross: int) -> str:
return f"trajets {rides} | recettes {gross // 100} EUR | TVA {self.vat(gross) // 100} EUR"
def extra(self) -> list[str]: # uses the hook
return ["vélos électriques : 41 % des trajets"]
def deliver(self) -> str:
return "envoyé à rapports@paris-mobilite.example"
if __name__ == "__main__":
for report in [LondonReport(), ParisReport()]:
print("\n".join(report.run("October")))
const RIDES: Record<string, [number, number]> = { london: [61_200, 4_896_000], paris: [22_400, 1_792_000] }; // rides, revenue in pence or cents (VAT included)
// The template method run() fixes the steps and their order; subclasses fill some of them in.
abstract class MonthlyReport {
protected abstract readonly city: string;
run(month: string): string[] {
const [rides, gross] = this.collect(); // shared step
const lines = [this.title(month)]; // must be filled in
lines.push(this.summary(rides, gross)); // must be filled in
lines.push(...this.extra()); // hook: optional, empty by default
lines.push(this.deliver()); // must be filled in
return lines;
}
protected collect(): [number, number] { return RIDES[this.city]; }
protected vat(gross: number): number { return Math.floor(gross / 6); } // 20% VAT included in the price
protected abstract title(month: string): string;
protected abstract summary(rides: number, gross: number): string;
protected extra(): string[] { return []; }
protected abstract deliver(): string;
}
class LondonReport extends MonthlyReport {
protected readonly city = "london";
protected title(month: string) { return `== London, ${month} ==`; }
protected summary(rides: number, gross: number) { return `rides ${rides} | revenue £${Math.floor(gross / 100)} | VAT £${Math.floor(this.vat(gross) / 100)}`; }
protected deliver() { return "sent to reports@london-council.example"; }
}
class ParisReport extends MonthlyReport {
protected readonly city = "paris";
private static readonly MONTHS: Record<string, string> = { October: "octobre" };
protected title(month: string) { return `== Paris, ${ParisReport.MONTHS[month]} ==`; }
protected summary(rides: number, gross: number) { return `trajets ${rides} | recettes ${Math.floor(gross / 100)} EUR | TVA ${Math.floor(this.vat(gross) / 100)} EUR`; }
protected extra() { return ["vélos électriques : 41 % des trajets"]; } // uses the hook
protected deliver() { return "envoyé à rapports@paris-mobilite.example"; }
}
for (const report of [new LondonReport(), new ParisReport()]) console.log(report.run("October").join("\n"));import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class Main {
static final Map<String, int[]> RIDES = Map.of("london", new int[] {61_200, 4_896_000}, "paris", new int[] {22_400, 1_792_000}); // rides, revenue in pence or cents (VAT included)
// The template method run() fixes the steps and their order; subclasses fill some of them in.
abstract static class MonthlyReport {
abstract String city();
final List<String> run(String month) { // final: subclasses can't reorder the steps
int[] data = collect(); // shared step
List<String> lines = new ArrayList<>();
lines.add(title(month)); // must be filled in
lines.add(summary(data[0], data[1])); // must be filled in
lines.addAll(extra()); // hook: optional, empty by default
lines.add(deliver()); // must be filled in
return lines;
}
int[] collect() { return RIDES.get(city()); }
static int vat(int gross) { return gross / 6; } // 20% VAT included in the price
abstract String title(String month);
abstract String summary(int rides, int gross);
List<String> extra() { return List.of(); }
abstract String deliver();
}
static class LondonReport extends MonthlyReport {
String city() { return "london"; }
String title(String month) { return "== London, " + month + " =="; }
String summary(int rides, int gross) { return "rides " + rides + " | revenue £" + gross / 100 + " | VAT £" + vat(gross) / 100; }
String deliver() { return "sent to reports@london-council.example"; }
}
static class ParisReport extends MonthlyReport {
static final Map<String, String> MONTHS = Map.of("October", "octobre");
String city() { return "paris"; }
String title(String month) { return "== Paris, " + MONTHS.get(month) + " =="; }
String summary(int rides, int gross) { return "trajets " + rides + " | recettes " + gross / 100 + " EUR | TVA " + vat(gross) / 100 + " EUR"; }
List<String> extra() { return List.of("vélos électriques : 41 % des trajets"); } // uses the hook
String deliver() { return "envoyé à rapports@paris-mobilite.example"; }
}
public static void main(String[] args) {
for (MonthlyReport report : List.of(new LondonReport(), new ParisReport())) System.out.println(String.join("\n", report.run("October")));
}
}Output (the same in all three languages)
== London, October == rides 61200 | revenue £48960 | VAT £8160 sent to reports@london-council.example == Paris, octobre == trajets 22400 | recettes 17920 EUR | TVA 2986 EUR vélos électriques : 41 % des trajets envoyé à rapports@paris-mobilite.example
6. Try it
Run a city's report
Choose a city. The table shows which class supplied each step that run() called, in order.
7. When not to use it
What you get
- No duplicated sequence: the order and shared steps exist once.
- Small subclasses: only what differs.
- Fixes land everywhere: change a shared step, every city gets it.
- Hooks give optional extension: without forcing every subclass to care.
What you pay
- Inheritance-bound: a report can only follow one template.
- Hard to follow: the flow jumps between base and subclass.
- Fragile base class: changing the template can break subclasses that relied on the old order.
- Too many hooks: a template with ten optional steps is hard to reason about.
| If… | Use instead |
|---|---|
| The varying part is one algorithm that callers choose | 13 Strategy, by composition |
| Steps should be combined freely, not in a fixed order | A pipeline of functions (see the Styles track: pipes and filters) |
| Only creation varies inside the sequence | 01 Factory method (a special case of this pattern) |
| There are only two variants that rarely change | A parameter might be enough |
8. How it goes wrong
ParisReport redefines run(), the shared order is gone. Make it final or document that it mustn't be overridden.summary() calls run(), you get recursion. Steps should be simple and local.9. Remember it and say it
How this hook works: what “The order of service is fixed” means
What the slogan means, phrase by phrase
- “The order of service”
- The template method
run(). - “is fixed”
- Steps always run in the same order.
- “their own parts”
- Abstract steps and optional hooks.
Why this picture fits: Every wedding at a venue follows its running order, but each couple's readings and music are their own.
The story → the concept
| The wedding | The code |
|---|---|
| The venue's running order | MonthlyReport.run() |
| Readings and speeches | title(), summary() |
| An optional extra dance | The extra() hook |
| The couple | LondonReport, ParisReport |
Watch it on an example
ParisReport().run("October")starts the base method.- It collects rides (base), then asks Paris for the title and summary.
- It calls the
extra()hook (Paris adds a line), then Paris'sdeliver().
The trap it saves you from
Copying a whole procedure to change two steps, then fixing a bug in only one copy.
It still works when…
Yes: test frameworks (setUp, the test, tearDown), web framework request handlers, and Java's AbstractList.
In an interview or code review
- Say what's fixed and what varies: "Every city report collects, titles, summarises and delivers in that order, so
run()is a template method; cities fill in the steps." - Name the hook:
extra()is optional with an empty default. - Mention the guard: mark
run()final in Java so subclasses can't reorder it. - Contrast: template method varies steps by inheritance; strategy varies an algorithm by composition.
10. Check yourself
What does the template method fix?
What is a hook?
The VAT rule changes. Where?
Who calls whom?
Template method vs strategy?
11. Go deeper
| Resource | Why |
|---|---|
| 13 Strategy | The composition-based alternative. |
Python unittest.TestCase | setUp, test, tearDown: a template method you fill in. |
Java AbstractList | Implement two methods and inherit the rest of a list. |
| Wikipedia: Template method pattern | Structure, hooks and history. |