Intent
Define the skeleton of an algorithm in a base class, leaving certain steps as overridable hooks. Subclasses customize the parts that vary without changing the overall flow.
The Problem
You're building data exporters. The CSV exporter does this:
- Fetch the rows.
- Apply user filters.
- Sort.
- Write a CSV header.
- Stream each row as CSV.
- Close the file.
Then product asks for a JSON exporter. Same steps 1–3. Different output: write a [, stream each row as a JSON object, write a ]. Then a PDF exporter — same first three, very different output.
The first cut copies the entire fetch-filter-sort logic into each exporter. Three months later there are five exporters and you've fixed the same off-by-one in the filter five times. Worse — when product asks "can we audit-log every export?" you're hunting the same touch-point across five files.
The shape: a workflow whose outline is identical across implementations, whose specific steps vary in well-understood places. That's exactly what Template Method was built for.
The Solution
Template Method says: write the workflow once in a base class as a single method (export()), and let it call abstract or default-able steps that subclasses override. The base class owns the order and enforcement of the workflow. Subclasses own only the steps that differ.
abstract class DataExporter
{
public final function export(Query $query, Stream $out): void
{
$rows = $this->fetch($query);
$rows = $this->applyFilters($rows);
$rows = $this->sort($rows);
$this->writeHeader($out);
foreach ($rows as $row) $this->writeRow($out, $row);
$this->writeFooter($out);
}
abstract protected function writeHeader(Stream $out): void;
abstract protected function writeRow(Stream $out, array $row): void;
protected function writeFooter(Stream $out): void { /* default: noop */ }
}
CsvExporter, JsonExporter, PdfExporter each become 30-line classes that override only writeHeader, writeRow, writeFooter. The shared workflow lives in one place. Adding "audit-log every export" is a one-line change inside export() — and every exporter inherits it for free.
Real-World Analogy
A recipe template — say, "make pasta." The shape is fixed: boil water, add pasta, cook, drain, sauce, serve. You don't get to skip the boiling step or do them in a different order; that's what makes it pasta.
But which pasta? Spaghetti, penne, fusilli — your choice. Which sauce? Marinara, carbonara, pesto — your choice. The recipe pins down the workflow; the chef fills in the substitutable bits.
That's Template Method in a sentence: shared shape, customizable bits, no rewriting the shape.
Structure
Three roles you'll see in every Template Method implementation:
- Abstract Class — owns the template method (the workflow). The template method is usually marked
finalso subclasses can't change the order. Holds abstract methods (must be overridden) and optional hooks (override if you want, default does nothing). - Concrete Class — extends the Abstract Class and fills in the abstract steps. May also override hooks for optional behavior.
- Client — calls the template method on a concrete instance and gets the full workflow. Doesn't see the steps — just the result.
The defining trick: the template method is final and concrete; the steps it calls are abstract or hooks. Inheritance is doing the work — and it's the GoF pattern most tightly bound to inheritance.
Code Examples
Here's a data-export workflow in five languages. Watch how the base class's export() is identical regardless of which subclass runs it — and how each subclass shrinks to "just the bits that differ."
abstract class DataExporter {
// The template method — final via TypeScript convention (don't override).
export(query: Query, out: WritableStream): void {
const rows = this.sort(this.applyFilters(this.fetch(query)));
this.writeHeader(out);
for (const row of rows) this.writeRow(out, row);
this.writeFooter(out);
}
protected fetch(query: Query): Row[] { return db.run(query); }
protected applyFilters(rows: Row[]): Row[] { return rows; }
protected sort(rows: Row[]): Row[] { return rows; }
protected abstract writeHeader(out: WritableStream): void;
protected abstract writeRow(out: WritableStream, row: Row): void;
protected writeFooter(_out: WritableStream): void { /* hook: noop */ }
}
class CsvExporter extends DataExporter {
protected writeHeader(out: WritableStream) { out.write("id,name,active\n"); }
protected writeRow(out: WritableStream, r: Row) { out.write(`${r.id},${r.name},${r.active}\n`); }
}
class JsonExporter extends DataExporter {
private first = true;
protected writeHeader(out: WritableStream) { out.write("["); }
protected writeRow(out: WritableStream, r: Row) {
out.write(this.first ? "" : ",");
this.first = false;
out.write(JSON.stringify(r));
}
protected writeFooter(out: WritableStream) { out.write("]"); }
}
from abc import ABC, abstractmethod
class DataExporter(ABC):
# The template method.
def export(self, query, out):
rows = self._sort(self._apply_filters(self._fetch(query)))
self._write_header(out)
for row in rows:
self._write_row(out, row)
self._write_footer(out)
def _fetch(self, query): return db.run(query)
def _apply_filters(self, rows): return rows
def _sort(self, rows): return rows
@abstractmethod
def _write_header(self, out): ...
@abstractmethod
def _write_row(self, out, row): ...
def _write_footer(self, out): pass # hook
class CsvExporter(DataExporter):
def _write_header(self, out): out.write("id,name,active\n")
def _write_row(self, out, row): out.write(f"{row.id},{row.name},{row.active}\n")
class JsonExporter(DataExporter):
def __init__(self):
self._first = True
def _write_header(self, out): out.write("[")
def _write_row(self, out, row):
if not self._first: out.write(",")
self._first = False
out.write(json.dumps(row))
def _write_footer(self, out): out.write("]")
public abstract class DataExporter {
// The template method — final so subclasses can't reorder the steps.
public final void export(Query query, OutputStream out) throws IOException {
List<Row> rows = sort(applyFilters(fetch(query)));
writeHeader(out);
for (Row row : rows) writeRow(out, row);
writeFooter(out);
}
protected List<Row> fetch(Query query) { return Db.run(query); }
protected List<Row> applyFilters(List<Row> r) { return r; }
protected List<Row> sort(List<Row> r) { return r; }
protected abstract void writeHeader(OutputStream out) throws IOException;
protected abstract void writeRow(OutputStream out, Row row) throws IOException;
protected void writeFooter(OutputStream out) throws IOException { /* hook */ }
}
public final class CsvExporter extends DataExporter {
@Override
protected void writeHeader(OutputStream out) throws IOException {
out.write("id,name,active\n".getBytes());
}
@Override
protected void writeRow(OutputStream out, Row row) throws IOException {
out.write((row.id + "," + row.name + "," + row.active + "\n").getBytes());
}
}
<?php
namespace App\Exports;
abstract class DataExporter
{
// The template method — final stops subclasses from changing the order.
final public function export(Query $query, Stream $out): void
{
$rows = $this->sort($this->applyFilters($this->fetch($query)));
$this->writeHeader($out);
foreach ($rows as $row) $this->writeRow($out, $row);
$this->writeFooter($out);
}
protected function fetch(Query $query): array { return Db::run($query); }
protected function applyFilters(array $rows): array { return $rows; }
protected function sort(array $rows): array { return $rows; }
abstract protected function writeHeader(Stream $out): void;
abstract protected function writeRow(Stream $out, array $row): void;
protected function writeFooter(Stream $out): void { /* hook */ }
}
final class CsvExporter extends DataExporter
{
protected function writeHeader(Stream $out): void { $out->write("id,name,active\n"); }
protected function writeRow(Stream $out, array $row): void
{
$out->write("{$row['id']},{$row['name']},{$row['active']}\n");
}
}
package exports
// Go has no inheritance — the idiomatic equivalent is composition.
// We expose a Run() function that takes a "Writer" interface; concrete
// exporters implement the Writer. The "template" is the Run function itself.
type RowWriter interface {
WriteHeader(out io.Writer) error
WriteRow(out io.Writer, row Row) error
WriteFooter(out io.Writer) error
}
func Export(query Query, out io.Writer, w RowWriter) error {
rows, err := sort(applyFilters(db.Run(query)))
if err != nil {
return err
}
if err := w.WriteHeader(out); err != nil { return err }
for _, row := range rows {
if err := w.WriteRow(out, row); err != nil { return err }
}
return w.WriteFooter(out)
}
type CsvWriter struct{}
func (CsvWriter) WriteHeader(out io.Writer) error { _, err := out.Write([]byte("id,name,active\n")); return err }
func (CsvWriter) WriteRow(out io.Writer, r Row) error {
_, err := fmt.Fprintf(out, "%d,%s,%t\n", r.ID, r.Name, r.Active)
return err
}
func (CsvWriter) WriteFooter(io.Writer) error { return nil }
// Use:
// err := Export(q, w, CsvWriter{})
The Go version is worth a second look. With no inheritance, the "template" becomes a function that accepts an interface — which is functionally identical, but reveals something the inheritance-based versions hide: Template Method is really just "function composition with a fixed shape and pluggable steps." In Go, the resemblance to plain old Strategy is unmistakable.
When to Use It
Reach for Template Method when you can answer "yes" to any of these:
- Multiple classes share a workflow with one or two variable steps. Exporters, importers, parsers, renderers, test runners with setUp/tearDown, queue workers — anywhere "fetch → process → emit" recurs.
- You want to enforce the shape of the algorithm. Subclasses can't reorder the steps; the base class is in charge. Useful when the order matters for correctness, security, or invariants.
- The shared part is bigger than the variable part. If only one out of seven steps changes per subclass, Template Method is the cleanest fit. If five out of seven change, you're shoehorning.
- You're already using an abstract base class for the family. Template Method composes naturally with Factory Method — the base class can both define the workflow and declare a
createX()hook for subclasses to fill.
If your variable parts swap at runtime rather than per subclass, you want Strategy instead. Same problem, composition flavor.
Pros and Cons
Pros
- The shared workflow lives in one place — fix a bug once, all subclasses benefit.
- Subclasses are tiny — they only carry what makes them different.
- The order and shape of the algorithm are enforced by the base class.
- Combines naturally with hooks for optional steps.
Cons
- Tied to inheritance — single inheritance limits how many "templates" a class can participate in.
- Liskov violations are easy. If a subclass needs to bend the workflow, you're stuck with overriding more than the steps — or breaking encapsulation by overriding the template itself.
- Variable behavior is fixed at compile time (or class-definition time). Strategy gives you runtime swap; Template Method does not.
- Discoverability is poor — readers have to jump from the base class's
export()to the subclass's overridden methods to follow what actually happens.
Pro Tips
- Mark the template method
final(or its language equivalent). It exists exactly because the order is fixed. If subclasses can override it, you've lost the protection the pattern was meant to give. - Distinguish abstract steps from hooks. Abstract steps must be overridden; hooks have a sensible default and are optional. Don't make everything abstract — that creates obligation noise for subclasses that don't care.
- Name the template method after the outcome, not the workflow.
export()is right;runExportPipeline()is leaking implementation. Callers shouldn't know about the template structure. - Keep step methods narrow and well-named.
writeHeader,writeRow,writeFooterare obvious.step1,step2,step3are not. - Don't multiply hooks. Each new hook is a new commitment to backwards-compatibility. If only one subclass needs a behavior, override its existing methods rather than adding a new hook to the base.
Relations with Other Patterns
- Factory Method is a specialised Template Method whose hole is "what kind of object should we create?" Template Method's holes can be anything; Factory Method's hole is creation specifically. They live in the same neighborhood and are often the same class.
- Strategy solves the same "swappable behavior" problem with composition instead of inheritance. Template Method picks at class-definition time; Strategy picks at call time. In modern code with DI containers, Strategy almost always reads better.
- Hook methods (the optional steps) are sometimes their own pattern. Frameworks like JUnit (
@Before,@After) and Rails (before_save,after_create) expose hooks as their public API while the framework owns the template. - Builder can be Template-Methodized: an abstract
Builderdefines the high-level construction sequence, and concrete builders fill in the steps.
Final Tips
The cleanest Template Method I've ever shipped was a base class for our background-job runner. The base class owned six steps: lock the job → run it with a timeout → record metrics → emit logs → ack on success → release on failure. Each concrete job class was 20 lines that only implemented run(). Adding observability or retries or audit logging meant editing one method on the base class — no edits to the dozens of concrete jobs.
That's the deep promise: a workflow you write once and a library of subclasses that lean on it. Reach for Template Method when you're about to copy-paste the same algorithm with one line changed — and always ask first whether Strategy would do the same job with less inheritance and more flexibility.


