Template Method puts an algorithm's skeleton in one place and lets subclasses supply the steps that vary. It is the pattern that runs against the general advice to prefer composition — and it earns that exception in one specific situation: when the order of the steps is a correctness invariant that no variant may break.
The problem
Three importers — CSV, JSON, XML — each do the same four things in the same order.
class CsvImporter {
run(file) {
const rows = this.parse(file);
validate(rows); // must happen before normalising
const clean = normalise(rows);
persist(clean);
audit(clean.length);
}
}
// JsonImporter and XmlImporter: the same five lines, copiedOnly parse differs. The other four lines are duplicated three times, and the duplication is not merely untidy — the order is a rule. Normalising before validating means invalid rows are silently coerced into valid-looking ones. A fourth importer written by someone who has not read the others will get it wrong eventually.
The pattern
class Importer {
run(file) { // final — the sequence, owned here
const rows = this.parse(file);
validate(rows);
const clean = normalise(rows);
persist(clean);
audit(clean.length);
}
parse(file) { throw new Error('subclasses must implement parse'); }
}
class CsvImporter extends Importer {
parse(file) { return parseCsv(file); }
}classDiagram
class Importer {
+run(file) void
#parse(file)* Row[]
#beforePersist(rows) void
}
class CsvImporter {
#parse(file) Row[]
}
class JsonImporter {
#parse(file) Row[]
#beforePersist(rows) void
}
Importer <|-- CsvImporter
Importer <|-- JsonImporterA new format supplies parse and inherits the invariant. No subclass has the opportunity to reorder anything, which is the property Strategy cannot provide.
Abstract steps and hooks
Two kinds of extension point, and conflating them is a common design error.
An abstract step must be implemented. Not implementing it is a compile error or an immediate exception, which is what you want for parse — an importer that cannot parse is not an importer.
A hook has a default, usually doing nothing, and subclasses override it if they wish: beforePersist(rows). Hooks are how the base class offers optional extension without forcing every subclass to care.
The design mistake is making everything abstract, which forces every subclass to write empty implementations, or making everything a hook, which lets a subclass silently do nothing where something was required.
The cost, stated plainly
Inheritance is the strongest coupling available. A subclass depends on the base class's protected surface, its call order, and its internal assumptions. Changing the base class can break every subclass in ways the compiler will not catch — this is the fragile base class problem, and it is why the pattern deserves scrutiny rather than default use.
A subclass cannot be reused elsewhere. CsvImporter's parsing is trapped inside an importer. If something else needs to parse CSV, it either duplicates or instantiates an importer it does not want.
Testing requires the whole pipeline. Testing CSV parsing means running run() with validation, persistence and audit, or creating a test-only subclass.
One dimension only. If importers must vary by both format and destination, Template Method gives you format × destination subclasses. That is Bridge's problem, and Template Method has no answer to it.
The composition alternative, and when it is better
Most Template Methods are better as a function taking the varying step:
function runImport(file, parse) {
const rows = parse(file);
validate(rows);
const clean = normalise(rows);
persist(clean);
audit(clean.length);
}
runImport(file, parseCsv);The sequence is still in one place and still unbreakable, parseCsv is independently reusable and testable, and there is no inheritance. Prefer this whenever the language allows it.
The inheritance version earns its place when there are several related extension points that must agree with each other — a parse step, a validation rule and an error format that belong to the same format — and grouping them in one class is genuinely clearer than passing four functions. That is a real case, and it is narrower than the pattern's popularity suggests.
Check your understanding
Common mistakes
Using it merely to remove duplication. If the steps could legitimately be reordered, a shared function is better and cheaper.
Calling overridable methods from the constructor. Subclass fields are not initialised yet.
Making every step abstract. Subclasses write empty implementations to satisfy the base class.
Making every step a hook. A subclass silently does nothing where something was required.
Deep hierarchies. A template method inherited three levels down means the sequence is assembled from three files, and no reader can state it.
Trying to vary two dimensions. Format × destination is a product of subclasses; that is Bridge's problem.
A base class that grows shared helpers. It becomes a utility bag that every subclass depends on, and the coupling spreads beyond the sequence.
Practice
A payment reconciliation job runs nightly for five providers. Each fetches a file, parses it, matches transactions, flags discrepancies and writes a report — same order every time, with only fetching and parsing differing. Design it, and say which parts you would deliberately not put in the template.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Separate the steps that must not be reordered from the steps that merely happen to be adjacent.
Show a strong answer
First, check that the order is genuinely an invariant, because that is the only thing that justifies the pattern. Matching before parsing is meaningless, and flagging discrepancies before matching is meaningless — those are real dependencies. Writing the report after flagging is also real. But fetching before parsing is a data dependency, not a rule, and it is worth noticing that the invariant covers steps two through five rather than all five.
The design I would build:
function reconcile(source) {
const raw = source.fetch();
const transactions = source.parse(raw);
const matches = match(transactions, ledger);
const discrepancies = flag(matches);
writeReport(discrepancies);
}The sequence lives in one function; each provider supplies a small object with fetch and parse. I would use the object rather than two loose functions precisely because of the criterion in this topic: fetching and parsing for one provider must agree with each other — the parser depends on what the fetcher produces, and the provider's date format and error conventions belong to both.
What I would deliberately keep out of the template:
- Retry and error handling for the fetch. Providers fail differently — one has a flaky SFTP endpoint, another rate-limits. Putting a single retry policy in the template forces a compromise that suits none of them, and putting a hook there invites five subtly different overrides. Let each source own its own fetching robustness; the template just calls
fetch(). - Scheduling. The template should be a function you can call, not a job that knows about cron. Otherwise it cannot be run for a single provider on demand, which is the first thing anyone will want during an incident.
- Report formatting. Flagging discrepancies is domain logic; rendering them is presentation. Keeping the template's last step as "produce discrepancies" and rendering outside means the same job can produce a file, an email and a dashboard feed without touching the sequence.
- Anything provider-specific about matching. If one provider needs a fuzzy amount match, the temptation is a hook. I would resist it — matching rules that vary belong as a parameter to
match, not as an override point, because a hook there lets a provider silently change what "matched" means.
Why not inheritance: there is one varying dimension, the steps are ordinary functions, and the sources are more useful outside the job — a support tool will want to fetch and parse a single file without reconciling. A subclass cannot offer that.
What would change my mind: if providers began differing in three or four coordinated ways, a class per provider would start to beat an object of loose functions on cohesion alone.
A base class has grown to 400 lines with 9 hooks, 3 abstract methods and 6 protected helpers, across 11 subclasses. Diagnose it and propose a way out.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Count how many of the extension points each subclass actually uses.
Show a strong answer
Diagnose by measuring usage before proposing anything. For each of the nine hooks, count the subclasses that override it. The distribution tells you what this is:
- A hook overridden by one subclass is not an extension point; it is a special case that was pushed into the base class to avoid a conditional. There are usually several of these, and each one made the base class the property of one subclass.
- A hook overridden by all eleven is not optional — it should be abstract, and the fact that it is not means some subclass may be silently relying on a default that is wrong for it.
- A hook overridden by none is dead and can be deleted immediately, which is the easiest win available.
The likely diagnosis: the base class stopped being a sequence and became a shared container. Six protected helpers is the giveaway — helpers are not part of an algorithm's skeleton, they are utilities, and their presence means subclasses depend on the base class for things unrelated to the ordering invariant. That is the coupling that makes the hierarchy hard to change.
The way out, in order:
- Delete the unused hooks. Free.
- Move the protected helpers out into plain functions or collaborators. They are the largest source of coupling and the easiest to remove, because nothing about them requires inheritance. This alone will shrink the base class substantially and let subclasses stop depending on it for utilities.
- Promote universally overridden hooks to abstract, one at a time, checking each subclass actually wanted its own behaviour rather than having drifted into it.
- Pull single-use hooks back into their subclass as ordinary methods called from that subclass's own step implementation, if the sequence allows. Where it does not, that subclass is fighting the template and may not belong in the hierarchy.
Then ask the real question: is there still an invariant? After the helpers leave and the special cases are removed, look at what remains of run(). If it is a genuine ordered sequence with two or three gaps, keep it — possibly converted to a function taking those steps. If it has become a loose collection of calls that variants reorder via hooks, the invariant is already gone, and the hierarchy is providing coupling with no guarantee in return. In that case the honest move is to delete the base class and let each of the eleven own its own flow, sharing the extracted functions.
How I would sequence the work: this is eleven subclasses, so nothing can be done in one change. Delete-unused first (no risk), then helpers (mechanical), then the judgement calls one subclass at a time. And I would write down the intended sequence as a comment or a test before starting, because it is currently knowable only by reading 400 lines.
Key takeaways
- Template Method puts an algorithm's skeleton in the base class and lets subclasses fill named gaps.
- Its unique contribution is an enforced ordering invariant — no variant can reorder or skip a step.
- Deduplication alone does not justify it. If a variant could legitimately reorder the steps, use a shared function.
- Distinguish abstract steps (must implement) from hooks (optional default). Making everything one or the other is a design error.
- Never call an overridable method from the constructor — the subclass's fields are not initialised yet.
- Costs: fragile base class coupling, steps that cannot be reused or tested alone, and one dimension of variation only.
- Prefer a function taking the varying step. Inheritance earns its place when several extension points must agree with each other.
Further reading
- Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).
- Robert C. Martin, Agile Software Development, Principles, Patterns, and Practices (Prentice Hall, 2002) — Template Method compared with Strategy.
- Leonid Mikhajlov and Emil Sekerinski, "A Study of the Fragile Base Class Problem" (ECOOP, 1998).
- Joshua Bloch, Effective Java, third edition (Addison-Wesley, 2018) — design for inheritance or prohibit it.
- Martin Fowler, Refactoring, second edition (Addison-Wesley, 2018) — form template method, and replacing it with delegation.