Template Method
Every team has a file like export.js. It started as one function that dumped rows to CSV. Then someone needed JSON, so they copied the function and changed one line. Then XML, same move. Three functions, ninety lines, and eighty of them identical.
Here are two of the three, trimmed to the shape that matters:
async function exportCsv(query) {
const rows = await db.rows(query);
const clean = rows.filter((r) => !r.deleted);
const body = toCsv(clean);
const out = header() + body + footer();
await writeFile("export.csv", out);
audit.log("export", { format: "csv", rows: clean.length });
}
async function exportJson(query) {
const rows = await db.rows(query);
const clean = rows.filter((r) => !r.deleted);
const body = toJson(clean);
const out = header() + body; // footer() never got copied here
await writeFile("export.json", out);
// ...and nobody logged this one, so it is invisible in the audit trail
}
Read them side by side and the duplication jumps out. Fetch the rows, drop the deleted ones, turn them into text, wrap the text, write the file, log the export. Every exporter does the same six things in the same order. Exactly one line, the serialize call, is genuinely different.
That would be harmless if the copies stayed in sync. They never do.
exportJson was copied before someone added the footer() call, so it ships files with no trailing newline and a downstream parser chokes on the last record about once a week. exportXml was copied after a flaky-disk incident, so it has a retry the other two never got. Nobody chose any of this. The copies drifted because there was no single place that said “this is what an export does.” The order of operations lived in three files, and three files is two too many.
The skeleton is the asset
The valuable thing here is not any one exporter. It is the order. Load, filter, serialize, frame, write, log. That sequence is what you want to write down once and never copy again. The serialize step is the only real variable.
So pull the sequence into one method and leave the variable step as a hole:
class Exporter {
// the template method: the whole algorithm, fixed, in one place
async run(query) {
const rows = await db.rows(query);
const clean = rows.filter((r) => !r.deleted);
const out = this.header() + this.serialize(clean) + this.footer();
await retry(() => writeFile(this.filename(), out));
audit.log("export", { rows: clean.length });
}
header() { return ""; } // hook: optional, default does nothing
footer() { return ""; } // hook: optional, default does nothing
serialize() { throw new Error("serialize() must be implemented"); }
filename() { throw new Error("filename() must be implemented"); }
}
run is the template method. It owns the algorithm. It calls the steps in a fixed order, and every step is either written right there (load, filter, write, log) or declared as a hole a subclass fills in (serialize, filename). A CSV exporter is now four lines, and not one of them is about the order of operations:
class CsvExporter extends Exporter {
serialize(rows) { return toCsv(rows); }
filename() { return "export.csv"; }
}
class JsonExporter extends Exporter {
serialize(rows) { return toJson(rows); }
filename() { return "export.json"; }
footer() { return "\n"; } // opt into the footer hook, on purpose
}
Add XML and you write one serialize and one filename. You cannot get the footer wrong, because you never touch the footer unless you mean to. You cannot forget the retry, because the retry lives in run and there is exactly one run. The steps that were drifting are now impossible to drift, because they exist in a single place.
That is the whole pattern. A base method fixes the skeleton of an algorithm; some steps are concrete and shared, some are holes the caller fills. The invariant structure lives in one spot, and only the parts that vary come from outside.
You have met this shape a hundred times without the name. It is every lifecycle method you have ever written. But first, the version of it that fits JavaScript better than the class does.
You rarely need the class
If you learned this pattern from a Java or C# book, it came wrapped in an abstract base class, a set of abstract methods, and a subclass per variant. That was the only way those languages let you supply a step, because you could not pass behavior around on its own. You had to pass an object with a method on it.
JavaScript never had that limitation. A function is already a value. So the “abstract method a subclass fills in” is just “a function the caller hands you.” No base class, no extends, no this. The skeleton is a plain function and the holes are its parameters:
async function runExport(query, {
serialize,
filename,
header = () => "", // the hook defaults live right here
footer = () => "",
}) {
const rows = await db.rows(query);
const clean = rows.filter((r) => !r.deleted);
const out = header() + serialize(clean) + footer();
await retry(() => writeFile(filename, out));
audit.log("export", { rows: clean.length });
}
Same six steps, same fixed order, same single required hole. A caller supplies serialize and filename, opts into footer if it wants, and touches nothing else:
await runExport(query, { serialize: toCsv, filename: "export.csv" });
await runExport(query, { serialize: toJson, filename: "export.json", footer: () => "\n" });
These two encodings do the identical job. The class leans on inheritance to deliver the step; the function leans on composition, passing the step in as an argument.
In modern JavaScript, reach for the function first. It is less machinery. It is easier to test, since you call it with a fake serialize and assert on the output without building a subclass. And it dodges the fragile-base-class trap covered later. Inheritance earns its keep when the steps share a pile of instance state, or when an is-a hierarchy is genuinely what you are modelling, but most template methods are happier as a function that takes its holes as arguments. That is composition over inheritance applied to a single algorithm.
Hooks: holes you do not have to fill
Notice header and footer shipped with defaults. Not every hole is mandatory. A hook is an optional step with a do-nothing default, a spot the skeleton offers you to plug into if you care and skips over if you do not.
That distinction matters. A required hole (serialize) means “I cannot run without this; you must supply it.” A hook (footer) means “there is a spot here; most callers ignore it.” The skeleton still runs the hook every time. It just runs an empty function when you left it alone.
The cleanest place to see hooks is a test runner. You have used these hooks even if you never called them a pattern:
import { before, beforeEach, afterEach, after, test } from "node:test";
let db;
before(async () => { db = await pool.connect(); }); // once, before everything
beforeEach(() => db.query("BEGIN")); // before each test
afterEach(() => db.query("ROLLBACK")); // after each test
after(() => db.release()); // once, at the very end
test("charges the card exactly once", async () => {
// the connection is open and wrapped in a fresh transaction, guaranteed
});
The runner owns the order. First before, then for every test beforeEach, then the test body, then afterEach, and after once at the end. You never call any of them. You register the ones you need and leave the rest empty, and an unregistered hook is a no-op the runner still dutifully invokes.
Web components are the same deal, one level down at the browser. You extend HTMLElement and override the lifecycle slots you need:
class CountdownTimer extends HTMLElement {
static observedAttributes = ["seconds"];
connectedCallback() { // the browser calls this when you insert the element
this.render();
this.timer = setInterval(() => this.tick(), 1000);
}
disconnectedCallback() { // ...and this when it is removed
clearInterval(this.timer);
}
attributeChangedCallback(name, oldValue, newValue) {
this.render(); // called when an observed attribute changes
}
}
You never write timer.connectedCallback(). The browser runs its element-upgrade algorithm, and at the point where the element joins the document it calls your connectedCallback. You override the slots that matter and leave adoptedCallback out entirely, because you do not care about that one, and the platform treats the missing slot as a no-op. React class lifecycles (componentDidMount, componentWillUnmount), Vue’s mounted, Svelte’s onMount: all the same shape. The framework owns the render loop, and your lifecycle methods are the holes.
That “you never call it yourself” is not trivia. It is the whole personality of the pattern.
Who is holding the remote
Template method and strategy look nearly identical in JavaScript. Both hand a function into a slot. The difference is the direction of the call, and it decides more than the syntax lets on.
With strategy, you are in charge. You hold a reference to a whole algorithm and you call it when you want. Swap the reference and the whole thing changes. You call it.
With template method, the skeleton is in charge. You hand it your steps and it decides when to run each one, in what order, and whether to run it at all. It calls you.
“Don’t call us, we’ll call you.” That line, sometimes called the Hollywood principle, is the smell of a template method. If you are filling named slots in a flow that something else runs, you are looking at template method. If you are holding a function and deciding when to call it, that is strategy.
Where you actually meet it
Once you know the shape, you see it everywhere the code you write is a passenger in someone else’s flow.
- Framework lifecycles. Everything with mount, update, and unmount. The framework owns the render loop; your lifecycle methods are the holes. This is the single most common way a working developer uses template method, usually without naming it.
- Build and CI pipelines. Install, build, test, deploy is a fixed skeleton, and each stage is a hook you fill per project. A CI config is a template method written in YAML: the runner decides the order and the failure semantics, you fill the steps.
- Data pipelines. Load, transform, format, output. Fixed spine, swappable middle. That is the demo below.
- Middleware-ish sequences. A fixed run order with per-step behavior. The middleware onion is a close cousin. So is chain of responsibility, where each step additionally decides whether the next one runs at all. Template method fixes which steps run; a chain lets any handler stop the line.
The playground makes the core move concrete. The skeleton (load then transform then format then output) never changes. You swap the two holes from the dropdowns and watch only the middle of the pipe change what comes out.
Change the transform and the same skeleton produces different rows. Change the format and it produces a different shape. The runPipeline function on line one never moved. That is the payoff: the order of operations is written once, and everything variable lives in the holes.
When the template turns on you
The pattern’s strength is that the flow lives in one place. Its failure mode is the same fact turned against you: the flow lives somewhere you are not looking.
And the plain over-engineering cases, which are the more common mistake:
There is only one implementation. A base class with a single subclass is not a template method. It is one function split across two files for no reason. Write the function. Extract the skeleton when the second real variant shows up and you can see what actually varies, instead of guessing.
The steps are not shared. If each “step” is used by exactly one caller, you invented a structure that is not there. The whole justification is that the fixed spine is common. If it is not common, there is nothing to hoist, and the ceremony just makes a straight-line function harder to read.
The honest test is the one from the top of this article. Do several callers run the same steps in the same order, differing only in a few spots? Then the skeleton is worth writing down once. If not, a plain function with a couple of parameters is the better tool, and reaching for a template is the over-engineering the pattern is famous for.
Summary
- Template method writes the fixed steps of an algorithm once and leaves holes for the parts that vary. The invariant order lives in one place, so the copies that used to drift cannot drift anymore.
- The classic form is a base class whose template method calls concrete steps and abstract holes. In JavaScript you rarely need it. A function that takes its varying steps as callbacks is the same pattern with less machinery, easier tests, and no fragile base class.
- A hook is an optional hole with a do-nothing default. The skeleton runs it every time; an unfilled hook simply does nothing. Test-runner
before/beforeEach/afterEach/afterand web-component lifecycle callbacks are hooks you already use. - The difference from strategy is who holds the control flow. Template method: the skeleton calls your step (“don’t call us, we’ll call you”). Strategy: you hold the function and call it yourself. In JS the code can look identical; the label is about direction, not syntax.
- You meet it as framework lifecycles, CI pipelines, data pipelines, and middleware sequences: any flow where your code is a passenger filling slots someone else runs.
- Use it when several callers run the same steps in the same order and differ only in a few spots. Avoid it when there is one implementation (just write the function), when the steps are not genuinely shared, or when a base class has sprouted so many hooks that the order of operations is no longer legible.