One order of steps that never changes, and steps that do: how a base class owns the skeleton, what a hook is, and when to hand the step in instead of inheriting it.
Tap Download statement in a banking app and pick CSV or PDF. Behind the button, both choices do the same four things in the same order: fetch the settled transactions, write the header, the rows and the footer, name the file, and send it. Only the writing differs. The trouble starts when the four things are written out twice, once per format, and someone changes one copy.
That is what the Template Method pattern is for: write the order of the steps once, in a base class, and let subclasses fill in the steps that differ. You have already met the shape without the name. The pub-sub logger has a base Processor whose handle calls an abstract process and then passes the record on, and chess has a base Piece.moves that calls each piece's own attacks. No lesson names it, so this one does, and it also answers a question the other lessons raise: Strategy varies an algorithm by handing in an object, and Template Method varies it by inheriting. When is each the right tool?
This lesson builds the exporter three times: as two copy-pasted functions, as a template method, and as a Strategy, and prints the same files from the last two.
A shop chain's opening checklist is Template Method: head office fixes the order of the steps, and each shop supplies its own preparation.
The first version: one function per format#
The first version writes one function per format. Each one fetches, formats and sends:
# export_v1.py: one function per format, each with its own copy of the steps
class Outbox:
"""Where finished files go. A stand-in that only reports what it was sent."""
def send(self, name: str, text: str) -> str:
return f"sent {name} ({len(text)} bytes)"
LEDGER = { # account -> (date, memo, amount in rupees, settled)
"A-1042": [
("2026-04-03", "Rent", -18000, True),
("2026-04-01", "Salary", 50000, True),
("2026-04-20", "Hotel hold", -500, False),
("2026-04-09", "Groceries", -2350, True),
("2026-04-15", "Refund", 1200, True),
],
}
def signed(amount: int) -> str:
return f"+{amount}" if amount > 0 else str(amount)
def export_csv(account: str, outbox: Outbox) -> str:
rows = sorted((r for r in LEDGER[account] if r[3]), key=lambda r: r[0]) # settled only, oldest first
lines = ["date,memo,amount"]
for date, memo, amount, _ in rows:
lines.append(f"{date},{memo},{amount}")
lines.append(f"total,,{sum(r[2] for r in rows)}")
text = "\n".join(lines)
return text + "\n" + outbox.send(f"{account}.csv", text)
def export_pdf(account: str, outbox: Outbox) -> str:
rows = sorted(LEDGER[account], key=lambda r: r[0]) # oldest first
lines = [f"STATEMENT {account}"]
for i, (date, memo, amount, _) in enumerate(rows):
if i % 2 == 0:
lines.append(f"-- page {i // 2 + 1} --")
lines.append(f"{date} {memo.ljust(10)} {signed(amount)}")
lines.append(f"TOTAL {signed(sum(r[2] for r in rows))}")
text = "\n".join(lines)
return text + "\n" + outbox.send(f"{account}.pdf", text)
outbox = Outbox()
print("-- csv --")
print(export_csv("A-1042", outbox))
print("-- pdf --")
print(export_pdf("A-1042", outbox))// ExportV1.java: one function per format, each with its own copy of the steps
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
class ExportV1 {
record Entry(String date, String memo, int amount, boolean settled) {}
// Where finished files go. A stand-in that only reports what it was sent.
static class Outbox {
String send(String name, String text) {
return "sent " + name + " (" + text.length() + " bytes)";
}
}
static final Map<String, List<Entry>> LEDGER = Map.of("A-1042", List.of(
new Entry("2026-04-03", "Rent", -18000, true),
new Entry("2026-04-01", "Salary", 50000, true),
new Entry("2026-04-20", "Hotel hold", -500, false),
new Entry("2026-04-09", "Groceries", -2350, true),
new Entry("2026-04-15", "Refund", 1200, true)));
static String signed(int amount) {
return amount > 0 ? "+" + amount : String.valueOf(amount);
}
static int sum(List<Entry> rows) {
int total = 0;
for (Entry r : rows) total += r.amount();
return total;
}
static String exportCsv(String account, Outbox outbox) {
List<Entry> rows = new ArrayList<>();
for (Entry r : LEDGER.get(account)) if (r.settled()) rows.add(r); // settled only
rows.sort(Comparator.comparing(Entry::date)); // oldest first
List<String> lines = new ArrayList<>();
lines.add("date,memo,amount");
for (Entry r : rows) {
lines.add(r.date() + "," + r.memo() + "," + r.amount());
}
lines.add("total,," + sum(rows));
String text = String.join("\n", lines);
return text + "\n" + outbox.send(account + ".csv", text);
}
static String exportPdf(String account, Outbox outbox) {
List<Entry> rows = new ArrayList<>(LEDGER.get(account));
rows.sort(Comparator.comparing(Entry::date)); // oldest first
List<String> lines = new ArrayList<>();
lines.add("STATEMENT " + account);
for (int i = 0; i < rows.size(); i++) {
Entry r = rows.get(i);
if (i % 2 == 0) {
lines.add("-- page " + (i / 2 + 1) + " --");
}
lines.add(r.date() + " " + String.format("%-10s", r.memo()) + " " + signed(r.amount()));
}
lines.add("TOTAL " + signed(sum(rows)));
String text = String.join("\n", lines);
return text + "\n" + outbox.send(account + ".pdf", text);
}
public static void main(String[] args) {
Outbox outbox = new Outbox();
System.out.println("-- csv --");
System.out.println(exportCsv("A-1042", outbox));
System.out.println("-- pdf --");
System.out.println(exportPdf("A-1042", outbox));
}
}// export_v1.cpp: one function per format, each with its own copy of the steps
#include <algorithm>
#include <iostream>
#include <map>
#include <string>
#include <vector>
struct Entry {
std::string date;
std::string memo;
int amount; // rupees
bool settled;
};
// Where finished files go. A stand-in that only reports what it was sent.
struct Outbox {
std::string send(const std::string& name, const std::string& text) const {
return "sent " + name + " (" + std::to_string(text.size()) + " bytes)";
}
};
const std::map<std::string, std::vector<Entry>> LEDGER = {
{"A-1042", {
{"2026-04-03", "Rent", -18000, true},
{"2026-04-01", "Salary", 50000, true},
{"2026-04-20", "Hotel hold", -500, false},
{"2026-04-09", "Groceries", -2350, true},
{"2026-04-15", "Refund", 1200, true}}},
};
std::string signedAmount(int amount) {
return amount > 0 ? "+" + std::to_string(amount) : std::to_string(amount);
}
int sum(const std::vector<Entry>& rows) {
int total = 0;
for (const Entry& r : rows) total += r.amount;
return total;
}
std::string join(const std::vector<std::string>& lines) {
std::string out;
for (size_t i = 0; i < lines.size(); i++) out += (i ? "\n" : "") + lines[i];
return out;
}
bool byDate(const Entry& a, const Entry& b) { return a.date < b.date; }
std::string exportCsv(const std::string& account, const Outbox& outbox) {
std::vector<Entry> rows;
for (const Entry& r : LEDGER.at(account)) if (r.settled) rows.push_back(r); // settled only
std::sort(rows.begin(), rows.end(), byDate); // oldest first
std::vector<std::string> lines = {"date,memo,amount"};
for (const Entry& r : rows) {
lines.push_back(r.date + "," + r.memo + "," + std::to_string(r.amount));
}
lines.push_back("total,," + std::to_string(sum(rows)));
std::string text = join(lines);
return text + "\n" + outbox.send(account + ".csv", text);
}
std::string exportPdf(const std::string& account, const Outbox& outbox) {
std::vector<Entry> rows = LEDGER.at(account);
std::sort(rows.begin(), rows.end(), byDate); // oldest first
std::vector<std::string> lines = {"STATEMENT " + account};
for (size_t i = 0; i < rows.size(); i++) {
if (i % 2 == 0) {
lines.push_back("-- page " + std::to_string(i / 2 + 1) + " --");
}
std::string memo = rows[i].memo;
memo.resize(std::max<size_t>(memo.size(), 10), ' ');
lines.push_back(rows[i].date + " " + memo + " " + signedAmount(rows[i].amount));
}
lines.push_back("TOTAL " + signedAmount(sum(rows)));
std::string text = join(lines);
return text + "\n" + outbox.send(account + ".pdf", text);
}
int main() {
Outbox outbox;
std::cout << "-- csv --\n" << exportCsv("A-1042", outbox) << "\n";
std::cout << "-- pdf --\n" << exportPdf("A-1042", outbox) << "\n";
}// exportV1.js: one function per format, each with its own copy of the steps
class Outbox {
// Where finished files go. A stand-in that only reports what it was sent.
send(name, text) {
return `sent ${name} (${text.length} bytes)`;
}
}
const LEDGER = {
// account -> [date, memo, amount in rupees, settled]
"A-1042": [
["2026-04-03", "Rent", -18000, true],
["2026-04-01", "Salary", 50000, true],
["2026-04-20", "Hotel hold", -500, false],
["2026-04-09", "Groceries", -2350, true],
["2026-04-15", "Refund", 1200, true],
],
};
const signed = (amount) => (amount > 0 ? `+${amount}` : String(amount));
const byDate = (a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
const sum = (rows) => rows.reduce((total, r) => total + r[2], 0);
function exportCsv(account, outbox) {
const rows = LEDGER[account].filter((r) => r[3]).sort(byDate); // settled only, oldest first
const lines = ["date,memo,amount"];
for (const [date, memo, amount] of rows) {
lines.push(`${date},${memo},${amount}`);
}
lines.push(`total,,${sum(rows)}`);
const text = lines.join("\n");
return text + "\n" + outbox.send(`${account}.csv`, text);
}
function exportPdf(account, outbox) {
const rows = [...LEDGER[account]].sort(byDate); // oldest first
const lines = [`STATEMENT ${account}`];
rows.forEach(([date, memo, amount], i) => {
if (i % 2 === 0) {
lines.push(`-- page ${Math.floor(i / 2) + 1} --`);
}
lines.push(`${date} ${memo.padEnd(10)} ${signed(amount)}`);
});
lines.push(`TOTAL ${signed(sum(rows))}`);
const text = lines.join("\n");
return text + "\n" + outbox.send(`${account}.pdf`, text);
}
const outbox = new Outbox();
console.log("-- csv --");
console.log(exportCsv("A-1042", outbox));
console.log("-- pdf --");
console.log(exportPdf("A-1042", outbox));-- csv --
date,memo,amount
2026-04-01,Salary,50000
2026-04-03,Rent,-18000
2026-04-09,Groceries,-2350
2026-04-15,Refund,1200
total,,30850
sent A-1042.csv (126 bytes)
-- pdf --
STATEMENT A-1042
-- page 1 --
2026-04-01 Salary +50000
2026-04-03 Rent -18000
-- page 2 --
2026-04-09 Groceries -2350
2026-04-15 Refund +1200
-- page 3 --
2026-04-20 Hotel hold -500
TOTAL +30350
sent A-1042.pdf (219 bytes)Both functions run, and both are short. Read the two outputs side by side, though: the CSV ends in a total of 30850 and the PDF in 30350, and the PDF shows a Hotel hold row of -500 that the CSV leaves out. The hold is not settled yet. Someone added the settled filter to export_csv when a customer complained, and nobody remembered that export_pdf has its own copy of the same line.
- A rule has to be changed in every copy, and one gets missed. The settled filter exists twice; the two files above already disagree by 500 rupees.
- The order of the steps lives in each function. To see what an export does (fetch, format, send) you read one function, and to be sure the others do the same, you read them all.
- A new format starts by copying. A third format copies the nearest function and edits it, and the copy inherits every old mistake.
- The part that varies is buried. Of a function's lines, only the ones that write a header, a row or a footer are really about CSV or PDF. The rest is the skeleton.
- The skeleton can't be tested once. Every format needs its own test for fetching, ordering and sending.
Same or different?
Look at export_csv and export_pdf. Which steps are the same in both, which differ, and which one has a step the other doesn't?
One strong answer
The same: fetching the rows, building header, rows and footer in that order, joining them, and sending the file. Different: how the header, each row and the footer are written, and the file's extension. Only the PDF has a step the CSV doesn't: a page mark before every second row. Fetching is meant to be the same, and the two copies already disagree about it (only the CSV keeps settled rows), which is the 500 rupees. So the skeleton is fixed and three or four steps vary, and one of the variations is optional. That is the whole shape of the pattern.
The pattern#
The book that named the pattern (Design Patterns, 1994, by Gamma, Helm, Johnson and Vlissides) states its intent in two sentences: "Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure."
In the exporter that gives four roles:
| Role | In the exporter | Its one job |
|---|---|---|
| Template method | Exporter.export_statement |
Call the steps in a fixed order. Callers use it; subclasses don't replace it |
| Fixed steps | fetch and the send |
Do the parts that are the same for every format, once |
| Abstract steps | extension, header, row, footer |
The parts every format must supply; a subclass that forgets one is an error |
| Hook | before_row |
A step with a default that does nothing; a subclass overrides it only if it has something to say |
%% title: Template Method: the exporter
classDiagram
class Exporter {
<<abstract>>
+export_statement(account) str
#fetch(account) list
#before_row(index) str
#extension() str
#header(account) str
#row(row) str
#footer(total) str
}
class Outbox {
+send(name, text) str
}
class CsvExporter
class PdfExporter
class MarkdownExporter
Exporter --> Outbox : sends to
Exporter <|-- CsvExporter
Exporter <|-- PdfExporter
Exporter <|-- MarkdownExporterRead the arrows. The triangles run from each format up to Exporter, so a format is an exporter: this is inheritance, where Strategy used a line from the context to an interface. The one public method is the template method (+); the steps are protected (#) because only the skeleton should call them. And notice the direction of the calls, which the diagram can't show: the base class calls the subclass. A format never calls the other steps itself; it waits to be called. The book notes that this inverted control is sometimes called "the Hollywood principle": don't call us, we'll call you.
The skeleton calls seven steps in one order: two are fixed, four are abstract and supplied by every format, and one is a do-nothing hook.
The code#
The skeleton goes in the base class. Each format overrides only what is its own:
# exporter.py: Template Method: the base class owns the order of the steps
from abc import ABC, abstractmethod
from typing import Optional
class Outbox:
"""Where finished files go. A stand-in that only reports what it was sent."""
def send(self, name: str, text: str) -> str:
return f"sent {name} ({len(text)} bytes)"
LEDGER = { # account -> (date, memo, amount in rupees, settled)
"A-1042": [
("2026-04-03", "Rent", -18000, True),
("2026-04-01", "Salary", 50000, True),
("2026-04-20", "Hotel hold", -500, False),
("2026-04-09", "Groceries", -2350, True),
("2026-04-15", "Refund", 1200, True),
],
}
def signed(amount: int) -> str:
return f"+{amount}" if amount > 0 else str(amount)
class Exporter(ABC):
def __init__(self, ledger: dict, outbox: Outbox) -> None:
self.ledger, self.outbox = ledger, outbox
def export_statement(self, account: str) -> str: # the template method: callers use this, subclasses never replace it
rows = self.fetch(account) # fixed step
lines = [self.header(account)] # abstract step
for i, row in enumerate(rows):
page_mark = self.before_row(i) # hook: does nothing unless overridden
if page_mark is not None:
lines.append(page_mark)
lines.append(self.row(row)) # abstract step
lines.append(self.footer(sum(r[2] for r in rows))) # abstract step
text = "\n".join(lines)
return text + "\n" + self.outbox.send(f"{account}.{self.extension}", text) # fixed step
def fetch(self, account: str) -> list:
return sorted((r for r in self.ledger[account] if r[3]), key=lambda r: r[0]) # settled only, oldest first
def before_row(self, index: int) -> Optional[str]:
return None
@property
@abstractmethod
def extension(self) -> str: ...
@abstractmethod
def header(self, account: str) -> str: ...
@abstractmethod
def row(self, row: tuple) -> str: ...
@abstractmethod
def footer(self, total: int) -> str: ...
class CsvExporter(Exporter):
extension = "csv"
def header(self, account: str) -> str:
return "date,memo,amount"
def row(self, row: tuple) -> str:
return f"{row[0]},{row[1]},{row[2]}"
def footer(self, total: int) -> str:
return f"total,,{total}"
class PdfExporter(Exporter):
extension = "pdf"
def header(self, account: str) -> str:
return f"STATEMENT {account}"
def before_row(self, index: int) -> Optional[str]:
return f"-- page {index // 2 + 1} --" if index % 2 == 0 else None
def row(self, row: tuple) -> str:
return f"{row[0]} {row[1].ljust(10)} {signed(row[2])}"
def footer(self, total: int) -> str:
return f"TOTAL {signed(total)}"
class MarkdownExporter(Exporter): # a new format: four small methods and nothing else
extension = "md"
def header(self, account: str) -> str:
return "| date | memo | amount |\n|---|---|---|"
def row(self, row: tuple) -> str:
return f"| {row[0]} | {row[1]} | {row[2]} |"
def footer(self, total: int) -> str:
return f"| total | | {total} |"
outbox = Outbox()
for name, exporter in [("csv", CsvExporter), ("pdf", PdfExporter), ("md", MarkdownExporter)]:
print(f"-- {name} --")
print(exporter(LEDGER, outbox).export_statement("A-1042"))// Exporter.java: Template Method: the base class owns the order of the steps
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
class ExporterDemo {
record Entry(String date, String memo, int amount, boolean settled) {}
// Where finished files go. A stand-in that only reports what it was sent.
static class Outbox {
String send(String name, String text) {
return "sent " + name + " (" + text.length() + " bytes)";
}
}
static final Map<String, List<Entry>> LEDGER = Map.of("A-1042", List.of(
new Entry("2026-04-03", "Rent", -18000, true),
new Entry("2026-04-01", "Salary", 50000, true),
new Entry("2026-04-20", "Hotel hold", -500, false),
new Entry("2026-04-09", "Groceries", -2350, true),
new Entry("2026-04-15", "Refund", 1200, true)));
static String signed(int amount) {
return amount > 0 ? "+" + amount : String.valueOf(amount);
}
static abstract class Exporter {
final Map<String, List<Entry>> ledger;
final Outbox outbox;
Exporter(Map<String, List<Entry>> ledger, Outbox outbox) {
this.ledger = ledger;
this.outbox = outbox;
}
// the template method: final, so callers use it and subclasses never replace it
final String exportStatement(String account) {
List<Entry> rows = fetch(account); // fixed step
List<String> lines = new ArrayList<>();
lines.add(header(account)); // abstract step
int total = 0;
for (int i = 0; i < rows.size(); i++) {
String pageMark = beforeRow(i); // hook: does nothing unless overridden
if (pageMark != null) {
lines.add(pageMark);
}
lines.add(row(rows.get(i))); // abstract step
total += rows.get(i).amount();
}
lines.add(footer(total)); // abstract step
String text = String.join("\n", lines);
return text + "\n" + outbox.send(account + "." + extension(), text); // fixed step
}
final List<Entry> fetch(String account) { // a fixed step: final, so no format can change it
List<Entry> rows = new ArrayList<>();
for (Entry r : ledger.get(account)) if (r.settled()) rows.add(r); // settled only
rows.sort(Comparator.comparing(Entry::date)); // oldest first
return rows;
}
String beforeRow(int index) {
return null;
}
abstract String extension();
abstract String header(String account);
abstract String row(Entry row);
abstract String footer(int total);
}
static class CsvExporter extends Exporter {
CsvExporter(Map<String, List<Entry>> ledger, Outbox outbox) { super(ledger, outbox); }
String extension() { return "csv"; }
String header(String account) { return "date,memo,amount"; }
String row(Entry r) { return r.date() + "," + r.memo() + "," + r.amount(); }
String footer(int total) { return "total,," + total; }
}
static class PdfExporter extends Exporter {
PdfExporter(Map<String, List<Entry>> ledger, Outbox outbox) { super(ledger, outbox); }
String extension() { return "pdf"; }
String header(String account) { return "STATEMENT " + account; }
@Override String beforeRow(int index) { return index % 2 == 0 ? "-- page " + (index / 2 + 1) + " --" : null; }
String row(Entry r) { return r.date() + " " + String.format("%-10s", r.memo()) + " " + signed(r.amount()); }
String footer(int total) { return "TOTAL " + signed(total); }
}
static class MarkdownExporter extends Exporter { // a new format: four small methods and nothing else
MarkdownExporter(Map<String, List<Entry>> ledger, Outbox outbox) { super(ledger, outbox); }
String extension() { return "md"; }
String header(String account) { return "| date | memo | amount |\n|---|---|---|"; }
String row(Entry r) { return "| " + r.date() + " | " + r.memo() + " | " + r.amount() + " |"; }
String footer(int total) { return "| total | | " + total + " |"; }
}
public static void main(String[] args) {
Outbox outbox = new Outbox();
Map<String, Exporter> exporters = new java.util.LinkedHashMap<>();
exporters.put("csv", new CsvExporter(LEDGER, outbox));
exporters.put("pdf", new PdfExporter(LEDGER, outbox));
exporters.put("md", new MarkdownExporter(LEDGER, outbox));
for (Map.Entry<String, Exporter> e : exporters.entrySet()) {
System.out.println("-- " + e.getKey() + " --");
System.out.println(e.getValue().exportStatement("A-1042"));
}
}
}// exporter.cpp: Template Method: the base class owns the order of the steps
#include <algorithm>
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <vector>
struct Entry {
std::string date;
std::string memo;
int amount; // rupees
bool settled;
};
using Ledger = std::map<std::string, std::vector<Entry>>;
// Where finished files go. A stand-in that only reports what it was sent.
struct Outbox {
std::string send(const std::string& name, const std::string& text) const {
return "sent " + name + " (" + std::to_string(text.size()) + " bytes)";
}
};
std::string signedAmount(int amount) {
return amount > 0 ? "+" + std::to_string(amount) : std::to_string(amount);
}
class Exporter {
public:
Exporter(const Ledger& ledger, const Outbox& outbox) : ledger_(ledger), outbox_(outbox) {}
virtual ~Exporter() = default;
// the template method: not virtual, so callers use it and subclasses never replace it
std::string exportStatement(const std::string& account) const {
std::vector<Entry> rows = fetch(account); // fixed step
std::string text = header(account); // abstract step
int total = 0;
for (size_t i = 0; i < rows.size(); i++) {
std::string pageMark = beforeRow(i); // hook: does nothing unless overridden
if (!pageMark.empty()) text += "\n" + pageMark;
text += "\n" + row(rows[i]); // abstract step
total += rows[i].amount;
}
text += "\n" + footer(total); // abstract step
return text + "\n" + outbox_.send(account + "." + extension(), text); // fixed step
}
protected:
std::vector<Entry> fetch(const std::string& account) const { // a fixed step: not virtual, so no format can change it
std::vector<Entry> rows;
for (const Entry& r : ledger_.at(account)) if (r.settled) rows.push_back(r); // settled only
std::sort(rows.begin(), rows.end(), [](const Entry& a, const Entry& b) { return a.date < b.date; }); // oldest first
return rows;
}
virtual std::string beforeRow(size_t) const { return ""; }
virtual std::string extension() const = 0;
virtual std::string header(const std::string& account) const = 0;
virtual std::string row(const Entry& entry) const = 0;
virtual std::string footer(int total) const = 0;
private:
const Ledger& ledger_;
const Outbox& outbox_;
};
class CsvExporter : public Exporter {
public:
using Exporter::Exporter;
protected:
std::string extension() const override { return "csv"; }
std::string header(const std::string&) const override { return "date,memo,amount"; }
std::string row(const Entry& e) const override { return e.date + "," + e.memo + "," + std::to_string(e.amount); }
std::string footer(int total) const override { return "total,," + std::to_string(total); }
};
class PdfExporter : public Exporter {
public:
using Exporter::Exporter;
protected:
std::string extension() const override { return "pdf"; }
std::string header(const std::string& account) const override { return "STATEMENT " + account; }
std::string beforeRow(size_t index) const override {
return index % 2 == 0 ? "-- page " + std::to_string(index / 2 + 1) + " --" : "";
}
std::string row(const Entry& e) const override {
std::string memo = e.memo;
memo.resize(std::max<size_t>(memo.size(), 10), ' ');
return e.date + " " + memo + " " + signedAmount(e.amount);
}
std::string footer(int total) const override { return "TOTAL " + signedAmount(total); }
};
class MarkdownExporter : public Exporter { // a new format: four small methods and nothing else
public:
using Exporter::Exporter;
protected:
std::string extension() const override { return "md"; }
std::string header(const std::string&) const override { return "| date | memo | amount |\n|---|---|---|"; }
std::string row(const Entry& e) const override { return "| " + e.date + " | " + e.memo + " | " + std::to_string(e.amount) + " |"; }
std::string footer(int total) const override { return "| total | | " + std::to_string(total) + " |"; }
};
int main() {
const Ledger ledger = {
{"A-1042", {
{"2026-04-03", "Rent", -18000, true},
{"2026-04-01", "Salary", 50000, true},
{"2026-04-20", "Hotel hold", -500, false},
{"2026-04-09", "Groceries", -2350, true},
{"2026-04-15", "Refund", 1200, true}}},
};
const Outbox outbox;
CsvExporter csv(ledger, outbox);
PdfExporter pdf(ledger, outbox);
MarkdownExporter md(ledger, outbox);
std::cout << "-- csv --\n" << csv.exportStatement("A-1042") << "\n";
std::cout << "-- pdf --\n" << pdf.exportStatement("A-1042") << "\n";
std::cout << "-- md --\n" << md.exportStatement("A-1042") << "\n";
}// exporter.js: Template Method: the base class owns the order of the steps
class Outbox {
// Where finished files go. A stand-in that only reports what it was sent.
send(name, text) {
return `sent ${name} (${text.length} bytes)`;
}
}
const LEDGER = {
// account -> [date, memo, amount in rupees, settled]
"A-1042": [
["2026-04-03", "Rent", -18000, true],
["2026-04-01", "Salary", 50000, true],
["2026-04-20", "Hotel hold", -500, false],
["2026-04-09", "Groceries", -2350, true],
["2026-04-15", "Refund", 1200, true],
],
};
const signed = (amount) => (amount > 0 ? `+${amount}` : String(amount));
class Exporter {
constructor(ledger, outbox) {
this.ledger = ledger;
this.outbox = outbox;
}
exportStatement(account) {
// the template method: callers use this, subclasses never replace it
const rows = this.fetch(account); // fixed step
const lines = [this.header(account)]; // abstract step
rows.forEach((row, i) => {
const pageMark = this.beforeRow(i); // hook: does nothing unless overridden
if (pageMark !== null) {
lines.push(pageMark);
}
lines.push(this.row(row)); // abstract step
});
lines.push(this.footer(rows.reduce((total, r) => total + r[2], 0))); // abstract step
const text = lines.join("\n");
return text + "\n" + this.outbox.send(`${account}.${this.extension}`, text); // fixed step
}
fetch(account) {
return this.ledger[account].filter((r) => r[3]).sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)); // settled only, oldest first
}
beforeRow(index) {
return null;
}
get extension() {
throw new Error("a subclass must define extension");
}
header(account) {
throw new Error("a subclass must define header");
}
row(row) {
throw new Error("a subclass must define row");
}
footer(total) {
throw new Error("a subclass must define footer");
}
}
class CsvExporter extends Exporter {
get extension() {
return "csv";
}
header(account) {
return "date,memo,amount";
}
row(row) {
return `${row[0]},${row[1]},${row[2]}`;
}
footer(total) {
return `total,,${total}`;
}
}
class PdfExporter extends Exporter {
get extension() {
return "pdf";
}
header(account) {
return `STATEMENT ${account}`;
}
beforeRow(index) {
return index % 2 === 0 ? `-- page ${Math.floor(index / 2) + 1} --` : null;
}
row(row) {
return `${row[0]} ${row[1].padEnd(10)} ${signed(row[2])}`;
}
footer(total) {
return `TOTAL ${signed(total)}`;
}
}
class MarkdownExporter extends Exporter {
// a new format: four small methods and nothing else
get extension() {
return "md";
}
header(account) {
return "| date | memo | amount |\n|---|---|---|";
}
row(row) {
return `| ${row[0]} | ${row[1]} | ${row[2]} |`;
}
footer(total) {
return `| total | | ${total} |`;
}
}
const outbox = new Outbox();
for (const [name, Format] of [["csv", CsvExporter], ["pdf", PdfExporter], ["md", MarkdownExporter]]) {
console.log(`-- ${name} --`);
console.log(new Format(LEDGER, outbox).exportStatement("A-1042"));
}-- csv --
date,memo,amount
2026-04-01,Salary,50000
2026-04-03,Rent,-18000
2026-04-09,Groceries,-2350
2026-04-15,Refund,1200
total,,30850
sent A-1042.csv (126 bytes)
-- pdf --
STATEMENT A-1042
-- page 1 --
2026-04-01 Salary +50000
2026-04-03 Rent -18000
-- page 2 --
2026-04-09 Groceries -2350
2026-04-15 Refund +1200
TOTAL +30850
sent A-1042.pdf (177 bytes)
-- md --
| date | memo | amount |
|---|---|---|
| 2026-04-01 | Salary | 50000 |
| 2026-04-03 | Rent | -18000 |
| 2026-04-09 | Groceries | -2350 |
| 2026-04-15 | Refund | 1200 |
| total | | 30850 |
sent A-1042.md (187 bytes)Check by hand. The ledger has five rows and one of them (the hold) is unsettled, so fetch returns four: 50000 - 18000 - 2350 + 1200 = 30850, the total all three formats print. The PDF's before_row returns a page mark for index 0 and index 2 and nothing for 1 and 3, which gives two pages. The CSV and Markdown formats never mention before_row, and need not: the base class's default returns nothing. The file sizes differ (126, 177 and 187 bytes) because the formats do, and the receipt line comes from the one send call that all three share.
What each language brings to the same design:
- Python: an
ABCwith@abstractmethodmakes a subclass that forgets a step fail the moment you instantiate it, not halfway through an export. Nothing stops a subclass from overridingexport_statementitself; that is a convention. - Java: an
abstractclass, andfinalonexportStatementand on the fixed stepfetch, which the compiler enforces: a subclass that tries to replace either doesn't compile. - C++: the template method and the fixed
fetchare ordinary non-virtual functions, and the steps a format may change areprotected virtual. A non-virtual skeleton can't be replaced (the idiom is called the non-virtual interface). - JavaScript: no
abstractand nofinal. The base class's steps throw "a subclass must define ..." so a missing step fails the first time it is called, and a subclass can replace the skeleton if it wants to.
One export, arrow by arrow: the skeleton fetches, asks the PDF for each step in order, and sends the file, while the PDF only answers.
Steps, and hooks#
Every step in the skeleton is one of three kinds, and choosing the kind is the real design work:
| Kind | Example | A subclass must... | Choose it when |
|---|---|---|---|
| Fixed | fetch, the send |
leave it alone | the step is the same for every subclass today and should stay that way |
| Abstract | row, footer |
supply it | every subclass has something of its own to say |
| Hook | before_row |
may override it | only some subclasses care; the default is "do nothing" |
A good test for abstract versus hook: does every subclass have something to say? Every format writes a row, so row is abstract. Only PDF has pages, so before_row is a hook with a default that returns nothing, and the other two formats don't mention it. If you make a hook abstract, every format has to write an empty method for it. If you make a real step a hook, a new format can forget it and silently produce a file without rows.
A format with no header
Operations asks for JSON lines: one JSON object per row, with no header and no footer. With the code above, what does a JsonLinesExporter have to do about header and footer, and what would you change in the base class?
One strong answer
It has to return an empty string for both, and the output then starts and ends with a blank line, because the skeleton joins every step's result with a newline. That's a sign that header and footer are really hooks, not abstract steps: not every format has one. The fix is in the base class: give both a default that returns nothing, and have the skeleton skip a missing result, as it already does for before_row. The existing formats keep working, because they override both.
Ask whether every subclass has something to say: if yes, the step is abstract; if only some do, it is a hook with a do-nothing default.
Inheritance, or hand the step in?#
Template Method and Strategy solve the same problem from opposite sides. Here is the same exporter written with Strategy: one concrete Exporter, and a Format object that is handed in.
# exporter_compose.py: the same export, with the format handed in instead of inherited
from typing import Optional
class Outbox:
"""Where finished files go. A stand-in that only reports what it was sent."""
def send(self, name: str, text: str) -> str:
return f"sent {name} ({len(text)} bytes)"
LEDGER = { # account -> (date, memo, amount in rupees, settled)
"A-1042": [
("2026-04-03", "Rent", -18000, True),
("2026-04-01", "Salary", 50000, True),
("2026-04-20", "Hotel hold", -500, False),
("2026-04-09", "Groceries", -2350, True),
("2026-04-15", "Refund", 1200, True),
],
}
def signed(amount: int) -> str:
return f"+{amount}" if amount > 0 else str(amount)
class Format: # what a format must answer; before_row has a default
extension = "?"
def header(self, account: str) -> str:
raise NotImplementedError
def row(self, row: tuple) -> str:
raise NotImplementedError
def footer(self, total: int) -> str:
raise NotImplementedError
def before_row(self, index: int) -> Optional[str]:
return None
class CsvFormat(Format):
extension = "csv"
def header(self, account: str) -> str:
return "date,memo,amount"
def row(self, row: tuple) -> str:
return f"{row[0]},{row[1]},{row[2]}"
def footer(self, total: int) -> str:
return f"total,,{total}"
class PdfFormat(Format):
extension = "pdf"
def header(self, account: str) -> str:
return f"STATEMENT {account}"
def before_row(self, index: int) -> Optional[str]:
return f"-- page {index // 2 + 1} --" if index % 2 == 0 else None
def row(self, row: tuple) -> str:
return f"{row[0]} {row[1].ljust(10)} {signed(row[2])}"
def footer(self, total: int) -> str:
return f"TOTAL {signed(total)}"
class MarkdownFormat(Format):
extension = "md"
def header(self, account: str) -> str:
return "| date | memo | amount |\n|---|---|---|"
def row(self, row: tuple) -> str:
return f"| {row[0]} | {row[1]} | {row[2]} |"
def footer(self, total: int) -> str:
return f"| total | | {total} |"
class Exporter: # one concrete class: the skeleton is here, the format is a parameter
def __init__(self, ledger: dict, outbox: Outbox, fmt: Format) -> None:
self.ledger, self.outbox, self.fmt = ledger, outbox, fmt
def export_statement(self, account: str) -> str:
rows = sorted((r for r in self.ledger[account] if r[3]), key=lambda r: r[0]) # settled only, oldest first
lines = [self.fmt.header(account)]
for i, row in enumerate(rows):
page_mark = self.fmt.before_row(i)
if page_mark is not None:
lines.append(page_mark)
lines.append(self.fmt.row(row))
lines.append(self.fmt.footer(sum(r[2] for r in rows)))
text = "\n".join(lines)
return text + "\n" + self.outbox.send(f"{account}.{self.fmt.extension}", text)
outbox = Outbox()
for name, fmt in [("csv", CsvFormat()), ("pdf", PdfFormat()), ("md", MarkdownFormat())]:
print(f"-- {name} --")
print(Exporter(LEDGER, outbox, fmt).export_statement("A-1042"))// ExporterCompose.java: the same export, with the format handed in instead of inherited
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
class ExporterCompose {
record Entry(String date, String memo, int amount, boolean settled) {}
// Where finished files go. A stand-in that only reports what it was sent.
static class Outbox {
String send(String name, String text) {
return "sent " + name + " (" + text.length() + " bytes)";
}
}
static final Map<String, List<Entry>> LEDGER = Map.of("A-1042", List.of(
new Entry("2026-04-03", "Rent", -18000, true),
new Entry("2026-04-01", "Salary", 50000, true),
new Entry("2026-04-20", "Hotel hold", -500, false),
new Entry("2026-04-09", "Groceries", -2350, true),
new Entry("2026-04-15", "Refund", 1200, true)));
static String signed(int amount) {
return amount > 0 ? "+" + amount : String.valueOf(amount);
}
interface Format { // what a format must answer; beforeRow has a default
String extension();
String header(String account);
String row(Entry row);
String footer(int total);
default String beforeRow(int index) { return null; }
}
static class CsvFormat implements Format {
public String extension() { return "csv"; }
public String header(String account) { return "date,memo,amount"; }
public String row(Entry r) { return r.date() + "," + r.memo() + "," + r.amount(); }
public String footer(int total) { return "total,," + total; }
}
static class PdfFormat implements Format {
public String extension() { return "pdf"; }
public String header(String account) { return "STATEMENT " + account; }
public String beforeRow(int index) { return index % 2 == 0 ? "-- page " + (index / 2 + 1) + " --" : null; }
public String row(Entry r) { return r.date() + " " + String.format("%-10s", r.memo()) + " " + signed(r.amount()); }
public String footer(int total) { return "TOTAL " + signed(total); }
}
static class MarkdownFormat implements Format {
public String extension() { return "md"; }
public String header(String account) { return "| date | memo | amount |\n|---|---|---|"; }
public String row(Entry r) { return "| " + r.date() + " | " + r.memo() + " | " + r.amount() + " |"; }
public String footer(int total) { return "| total | | " + total + " |"; }
}
static class Exporter { // one concrete class: the skeleton is here, the format is a parameter
final Map<String, List<Entry>> ledger;
final Outbox outbox;
final Format fmt;
Exporter(Map<String, List<Entry>> ledger, Outbox outbox, Format fmt) {
this.ledger = ledger;
this.outbox = outbox;
this.fmt = fmt;
}
String exportStatement(String account) {
List<Entry> rows = new ArrayList<>();
for (Entry r : ledger.get(account)) if (r.settled()) rows.add(r); // settled only
rows.sort(Comparator.comparing(Entry::date)); // oldest first
List<String> lines = new ArrayList<>();
lines.add(fmt.header(account));
int total = 0;
for (int i = 0; i < rows.size(); i++) {
String pageMark = fmt.beforeRow(i);
if (pageMark != null) {
lines.add(pageMark);
}
lines.add(fmt.row(rows.get(i)));
total += rows.get(i).amount();
}
lines.add(fmt.footer(total));
String text = String.join("\n", lines);
return text + "\n" + outbox.send(account + "." + fmt.extension(), text);
}
}
public static void main(String[] args) {
Outbox outbox = new Outbox();
Map<String, Format> formats = new LinkedHashMap<>();
formats.put("csv", new CsvFormat());
formats.put("pdf", new PdfFormat());
formats.put("md", new MarkdownFormat());
for (Map.Entry<String, Format> e : formats.entrySet()) {
System.out.println("-- " + e.getKey() + " --");
System.out.println(new Exporter(LEDGER, outbox, e.getValue()).exportStatement("A-1042"));
}
}
}// exporter_compose.cpp: the same export, with the format handed in instead of inherited
#include <algorithm>
#include <iostream>
#include <map>
#include <string>
#include <vector>
struct Entry {
std::string date;
std::string memo;
int amount; // rupees
bool settled;
};
using Ledger = std::map<std::string, std::vector<Entry>>;
// Where finished files go. A stand-in that only reports what it was sent.
struct Outbox {
std::string send(const std::string& name, const std::string& text) const {
return "sent " + name + " (" + std::to_string(text.size()) + " bytes)";
}
};
std::string signedAmount(int amount) {
return amount > 0 ? "+" + std::to_string(amount) : std::to_string(amount);
}
struct Format { // what a format must answer; beforeRow has a default
virtual ~Format() = default;
virtual std::string extension() const = 0;
virtual std::string header(const std::string& account) const = 0;
virtual std::string row(const Entry& entry) const = 0;
virtual std::string footer(int total) const = 0;
virtual std::string beforeRow(size_t) const { return ""; }
};
struct CsvFormat : Format {
std::string extension() const override { return "csv"; }
std::string header(const std::string&) const override { return "date,memo,amount"; }
std::string row(const Entry& e) const override { return e.date + "," + e.memo + "," + std::to_string(e.amount); }
std::string footer(int total) const override { return "total,," + std::to_string(total); }
};
struct PdfFormat : Format {
std::string extension() const override { return "pdf"; }
std::string header(const std::string& account) const override { return "STATEMENT " + account; }
std::string beforeRow(size_t index) const override {
return index % 2 == 0 ? "-- page " + std::to_string(index / 2 + 1) + " --" : "";
}
std::string row(const Entry& e) const override {
std::string memo = e.memo;
memo.resize(std::max<size_t>(memo.size(), 10), ' ');
return e.date + " " + memo + " " + signedAmount(e.amount);
}
std::string footer(int total) const override { return "TOTAL " + signedAmount(total); }
};
struct MarkdownFormat : Format {
std::string extension() const override { return "md"; }
std::string header(const std::string&) const override { return "| date | memo | amount |\n|---|---|---|"; }
std::string row(const Entry& e) const override { return "| " + e.date + " | " + e.memo + " | " + std::to_string(e.amount) + " |"; }
std::string footer(int total) const override { return "| total | | " + std::to_string(total) + " |"; }
};
class Exporter { // one concrete class: the skeleton is here, the format is a parameter
public:
Exporter(const Ledger& ledger, const Outbox& outbox, const Format& fmt) : ledger_(ledger), outbox_(outbox), fmt_(fmt) {}
std::string exportStatement(const std::string& account) const {
std::vector<Entry> rows;
for (const Entry& r : ledger_.at(account)) if (r.settled) rows.push_back(r); // settled only
std::sort(rows.begin(), rows.end(), [](const Entry& a, const Entry& b) { return a.date < b.date; }); // oldest first
std::string text = fmt_.header(account);
int total = 0;
for (size_t i = 0; i < rows.size(); i++) {
std::string pageMark = fmt_.beforeRow(i);
if (!pageMark.empty()) text += "\n" + pageMark;
text += "\n" + fmt_.row(rows[i]);
total += rows[i].amount;
}
text += "\n" + fmt_.footer(total);
return text + "\n" + outbox_.send(account + "." + fmt_.extension(), text);
}
private:
const Ledger& ledger_;
const Outbox& outbox_;
const Format& fmt_;
};
int main() {
const Ledger ledger = {
{"A-1042", {
{"2026-04-03", "Rent", -18000, true},
{"2026-04-01", "Salary", 50000, true},
{"2026-04-20", "Hotel hold", -500, false},
{"2026-04-09", "Groceries", -2350, true},
{"2026-04-15", "Refund", 1200, true}}},
};
const Outbox outbox;
const CsvFormat csv;
const PdfFormat pdf;
const MarkdownFormat md;
std::cout << "-- csv --\n" << Exporter(ledger, outbox, csv).exportStatement("A-1042") << "\n";
std::cout << "-- pdf --\n" << Exporter(ledger, outbox, pdf).exportStatement("A-1042") << "\n";
std::cout << "-- md --\n" << Exporter(ledger, outbox, md).exportStatement("A-1042") << "\n";
}// exporterCompose.js: the same export, with the format handed in instead of inherited
class Outbox {
// Where finished files go. A stand-in that only reports what it was sent.
send(name, text) {
return `sent ${name} (${text.length} bytes)`;
}
}
const LEDGER = {
// account -> [date, memo, amount in rupees, settled]
"A-1042": [
["2026-04-03", "Rent", -18000, true],
["2026-04-01", "Salary", 50000, true],
["2026-04-20", "Hotel hold", -500, false],
["2026-04-09", "Groceries", -2350, true],
["2026-04-15", "Refund", 1200, true],
],
};
const signed = (amount) => (amount > 0 ? `+${amount}` : String(amount));
class Format {
// what a format must answer; beforeRow has a default
get extension() {
throw new Error("a format must define extension");
}
header(account) {
throw new Error("a format must define header");
}
row(row) {
throw new Error("a format must define row");
}
footer(total) {
throw new Error("a format must define footer");
}
beforeRow(index) {
return null;
}
}
class CsvFormat extends Format {
get extension() {
return "csv";
}
header(account) {
return "date,memo,amount";
}
row(row) {
return `${row[0]},${row[1]},${row[2]}`;
}
footer(total) {
return `total,,${total}`;
}
}
class PdfFormat extends Format {
get extension() {
return "pdf";
}
header(account) {
return `STATEMENT ${account}`;
}
beforeRow(index) {
return index % 2 === 0 ? `-- page ${Math.floor(index / 2) + 1} --` : null;
}
row(row) {
return `${row[0]} ${row[1].padEnd(10)} ${signed(row[2])}`;
}
footer(total) {
return `TOTAL ${signed(total)}`;
}
}
class MarkdownFormat extends Format {
get extension() {
return "md";
}
header(account) {
return "| date | memo | amount |\n|---|---|---|";
}
row(row) {
return `| ${row[0]} | ${row[1]} | ${row[2]} |`;
}
footer(total) {
return `| total | | ${total} |`;
}
}
class Exporter {
// one concrete class: the skeleton is here, the format is a parameter
constructor(ledger, outbox, fmt) {
this.ledger = ledger;
this.outbox = outbox;
this.fmt = fmt;
}
exportStatement(account) {
const rows = this.ledger[account].filter((r) => r[3]).sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)); // settled only, oldest first
const lines = [this.fmt.header(account)];
rows.forEach((row, i) => {
const pageMark = this.fmt.beforeRow(i);
if (pageMark !== null) {
lines.push(pageMark);
}
lines.push(this.fmt.row(row));
});
lines.push(this.fmt.footer(rows.reduce((total, r) => total + r[2], 0)));
const text = lines.join("\n");
return text + "\n" + this.outbox.send(`${account}.${this.fmt.extension}`, text);
}
}
const outbox = new Outbox();
for (const [name, fmt] of [["csv", new CsvFormat()], ["pdf", new PdfFormat()], ["md", new MarkdownFormat()]]) {
console.log(`-- ${name} --`);
console.log(new Exporter(LEDGER, outbox, fmt).exportStatement("A-1042"));
}-- csv --
date,memo,amount
2026-04-01,Salary,50000
2026-04-03,Rent,-18000
2026-04-09,Groceries,-2350
2026-04-15,Refund,1200
total,,30850
sent A-1042.csv (126 bytes)
-- pdf --
STATEMENT A-1042
-- page 1 --
2026-04-01 Salary +50000
2026-04-03 Rent -18000
-- page 2 --
2026-04-09 Groceries -2350
2026-04-15 Refund +1200
TOTAL +30850
sent A-1042.pdf (177 bytes)
-- md --
| date | memo | amount |
|---|---|---|
| 2026-04-01 | Salary | 50000 |
| 2026-04-03 | Rent | -18000 |
| 2026-04-09 | Groceries | -2350 |
| 2026-04-15 | Refund | 1200 |
| total | | 30850 |
sent A-1042.md (187 bytes)It prints exactly what the template version printed, line for line. So the question is not which one works, but which one fits:
Inherit them: Template Method
- The order is written once in the base class, and Java and C++ can forbid replacing it
- A step can use the base class's protected helpers and fields directly
- The format is fixed when the object is built and can't change afterwards
- A step can't be reused by another exporter without inheriting from this one
Hand them in: Strategy
- The order is written once in
Exporter, a concrete class; a format can't replace it, because a format is not an exporter - A format is a separate object: testable alone, reusable, and swappable at runtime
- A format sees only what it is given, so it can't borrow the exporter's internals
- One more object to build and wire; the choice moves to whoever constructs the exporter
Both designs keep the order in one class, so that is not what decides between them. A useful rule: inherit when the steps need the base class's own state and helpers; hand the step in when the variation is the thing you want to swap or test on its own. If the format comes from a button the user presses, you'll build it from a name, and a registry of formats (as in Factory Method) works for both designs.
Two more differences matter in practice. First, inheritance couples a subclass to the base class's inner workings. PdfExporter.before_row works only because the base class calls it once per row, before the row; change that call in the base class and PdfExporter breaks without a compiler error. A handed-in Format depends on an interface, which is a smaller promise. Second, the pattern is often a function in disguise. In Python and JavaScript a skeleton that takes three functions (header, row, footer) does the job with no classes at all; reach for classes when the steps share settings or when there are more than three or four of them.
Which design?
Two requirements arrive. (a) A user picks the export format from a dropdown, and the same exporter should be able to export the same statement in two formats in a row. (b) Every export, whatever the format, must write an audit line, and no format may skip it. Which design suits which?
One strong answer
(a) wants the format handed in. (b) works in either design, so it doesn't decide. For (a) the format is data the caller picks at runtime, so it should be an object you pass (to the constructor, as above, or to export_statement itself if one exporter must switch), not a subclass fixed when the exporter is built. For (b), put the audit line in the skeleton. With inheritance, make the template method final in Java or non-virtual in C++; in Python and JavaScript the language can't forbid an override, so it's a convention plus a test. With a handed-in format, a format can't skip the audit at all, because it never owns the skeleton. So the requirement that decides is (a): does the variation change at runtime?
Template Method and its look-alikes#
| Pattern | Looks like Template Method because | The difference |
|---|---|---|
| Strategy (dp-16) | one algorithm, varying parts | The book's line: template methods use inheritance to vary part of an algorithm, strategies use delegation to vary the entire algorithm. The handed-in Format above sits between the two: delegation, but of steps. |
| Factory Method (dp-07) | a subclass overrides a method the base class calls | Factory Method is the special case where the overridden step creates an object. The book notes that factory methods are often called from template methods. |
| State (dp-19) | a base class with default behaviour and overriding subclasses | A state's methods are independent answers to different events; no method calls another. See the next section. |
| Decorator (dp-11) | extra behaviour around a core | A decorator wraps another object of the same interface; a template method calls into subclasses from its own body. |
| Chain of Responsibility (dp-21) | a fixed step calls a variable one | The logger's Processor.handle is a template method (process, then pass on) inside a chain; the chain is the pattern, the handle skeleton is the shape. |
Is the vending machine's base State a template method?#
No, and it is worth being clear about, because the base class looks the part. In the vending machine, the base State has four methods (insert, select, cancel, item_dropped), each with a default answer such as "coin returned" or "can't select now", and each concrete state overrides the events it handles. That is default behaviour: a state that overrides nothing refuses everything. But no method of State calls another, there is no fixed sequence of steps, and nothing is "the skeleton". Each method is an independent reply to a different event, so the base class is an interface with defaults, which is State.
The test is easy to apply. Look in the base class for a method whose body calls two or more of the overridable methods in a set order. If there is one, that is the template method. If the base class only supplies defaults, it is not.
When not to use it#
- There is one format and no second in sight. Write the function. The second format is the time to extract the skeleton, and it's a small, safe change.
- The variation is chosen at runtime. A dropdown, a plan or a config value picks a Strategy, not a subclass.
- The hierarchy is already deep. Template Method on top of three levels of inheritance makes every step's origin hard to find. Prefer handing the step in.
- The skeleton has two steps. "Do A, then B" isn't worth a base class.
- Subclasses need to change the order. Then the order is not fixed, and you have a toolbox of helper functions, not a template.
One more trap lives in constructors. Calling an overridable step from a base-class constructor is a bug in waiting: in C++ the call runs the base version (the derived part doesn't exist yet), and in Java, JavaScript and (when the subclass calls super().__init__() first) Python it runs the subclass version before the subclass's own fields are set. Do the work in the template method, after the object is built.
Where it shows up in the case studies#
No case study names Template Method, and none uses it as a whole. Two contain the shape, and a few use the opposite choice:
| Case study | What it has | Template Method? |
|---|---|---|
| Pub-sub logger | Processor.handle calls the abstract process, then passes the result to next |
The shape, yes. handle is the skeleton, process is the step each link supplies |
| Chess | Piece.moves calls the abstract attacks and keeps the squares that don't hold a friend; the pawn overrides moves |
The shape, yes, with the pawn replacing the whole skeleton, which the pattern allows when the language doesn't forbid it |
| Vending machine | base State with default replies |
No: defaults, not a skeleton (see above) |
| Parking lot | PricingStrategy, SpotAssignmentStrategy |
No, the opposite choice: the varying step is handed in |
| File system | every operation repeats normalise, walk, check, mutate | A candidate. The steps repeat by convention, with no shared skeleton; the lesson calls it "one shape for every operation" |
In an interview, the honest line is that Template Method is rarely the headline pattern, and often the right small one: a base class that owns an order of steps.
In the interview#
Say the pattern with the reason, in one breath: "The export always fetches, formats and delivers in that order, and only the formatting varies, so I'll put the order in a base class as a template method and let each format override the steps. If the format needs to change at runtime, I'll hand it in as a strategy instead." Saying which of the two you chose, and why, is what scores.
Then expect the follow-ups:
- "Add a format." A new subclass with four small methods. The skeleton and the existing formats don't change.
- "Some formats have no header." Make
headera hook with a default, not an abstract step; the rule is whether every subclass has something to say. - "How do you stop a subclass changing the order?"
finalin Java, a non-virtual function in C++. In Python and JavaScript it is a convention, so say so, and test the order. - "Template Method or Strategy?" Inherit when the steps need the base class's own state and helpers; hand in when the variation is what you swap or test alone. Both keep the order in one place.
- "How do you test it?" Test the skeleton once, with a tiny subclass that records the order its steps were called in. Then test each format's
rowandfooteron their own. - "What if sending fails?" The skeleton decides: wrap the send in the base class, so every format gets the same error handling and no format can forget it.
Common mistakes#
- Overriding the template method. A subclass that replaces
export_statementskips the skeleton and, with it, whatever the base class guarantees, such as sending the file. - Making every step abstract. A step that most subclasses leave empty is a hook, and an abstract method that returns an empty string is a smell.
- Making a real step a hook. A format that silently forgets
rowproduces a file with no rows. - Subclasses calling each other's steps. If
PdfExporter.rowcallsCsvExporter.row, the hierarchy has become a tangle; share a helper in the base class. - Calling a step from a constructor. The subclass isn't ready; do the work in the template method.
- A deep hierarchy. If subclasses have subclasses with their own overrides, hand the step in.
- Reaching for it before there is a second case. One format needs a function.
Overriding the template method skips whatever the base class guarantees, such as sending the file; Java's final and a non-virtual C++ function forbid it.
Template Method
Q1/4 Which of these is the template method in the exporter?
Answer & explanation
Answer: C
The template method is the one that fixes the order: it calls fetch, header, the rows, footer and send. The others are the steps it calls. before_row is a hook, which is one kind of step.
Q2/4 Only the PDF format needs page marks. What should
before_rowbe in the base class?Answer & explanation
Answer: B
A step that only some subclasses care about is a hook. Making it abstract forces every format to write an empty method; making it PDF-only means the skeleton can't call it.
Q3/4 A user can pick CSV or PDF from a dropdown, and the same exporter must produce both in a row. Which design fits better?
Answer & explanation
Answer: B
When the variation is chosen at runtime, it should be an object you pass in. A subclass is fixed when the object is created, and an if brings back the first version's problem.
Q4/4 The vending machine's base
Statehas default replies such as "can't select now". Is it a template method?Answer & explanation
Answer: C
A template method is a method that calls overridable steps in a set order. State's four methods are independent replies to four different events, so the base class only supplies defaults. Overriding alone doesn't make a template method.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


