Skip to article
ALGORITHMICSPatterns
Patterns5 min read

Template Method

The skeleton fixed, the steps varied — inheritance's one genuinely good use, and its limits.


Three report generators. Each one fetches rows, formats them, and returns a string — surrounded by identical retry logic, identical timing, and identical error wrapping.

What people write first

Copy the pipeline, change the two lines that differ.

class CsvReport {
  async run() {
    const started = Date.now();
    for (let attempt = 0; ; attempt++) {
      try {
        const rows = await db.query(CSV_SQL);      // ← differs
        const body = toCsv(rows);                  // ← differs
        metrics.timing("report", Date.now() - started);
        return body;
      } catch (e) {
        if (attempt >= 2) throw new ReportFailed(e);
      }
    }
  }
}

// …and PdfReport, and JsonReport, each with the same 12 lines around it.

The retry, the timing and the error wrapping are duplicated. Fix a bug in one and the other two keep it.

Twelve lines duplicated three times. It works until the retry limit changes, or someone discovers the metric name is wrong, and the fix lands in one of the three.

The pattern

Write the sequence once, in a method nobody overrides. Leave holes.

abstract class Report {
/** The skeleton. Not overridable — that is the whole idea. */
async run(): Promise<string> {
const started = Date.now();
for (let attempt = 0; ; attempt += 1) {
try {
const body = this.format(await this.fetch());
metrics.timing('report', Date.now() - started);
return body;
} catch (error) {
if (attempt >= 2) throw new ReportFailed(error);
}
}
}
// The holes. A subclass must fill both.
protected abstract fetch(): Promise<Row[]>;
protected abstract format(rows: Row[]): string;
}
class CsvReport extends Report {
protected fetch() { return db.query(CSV_SQL); }
protected format(rows: Row[]) { return toCsv(rows); }
}

Hooks: the optional holes

Not every step must be mandatory. A hook is a step with a default the subclass may override:

abstract class Report {
async run() { /* … */ }
protected abstract fetch(): Promise<Row[]>;
protected abstract format(rows: Row[]): string;
/** Hook: most reports do not need this. */
protected shouldCache(): boolean { return true; }
}

Keep abstract methods for what genuinely varies and hooks for what occasionally does. A base class with eight hooks is a base class that does not know what it is.

Where it runs out

Inheritance gives you one axis of variation, chosen at compile time.

Two independent axes — three fetch strategies × three formats — means nine subclasses. That is the class explosion again, and the answer is the same as it was for Decorator: compose instead.

class Report {
constructor(
private readonly fetch: () => Promise<Row[]>,
private readonly format: (rows: Row[]) => string,
) {}
async run(): Promise<string> { /* identical skeleton */ }
}
new Report(csvQuery, toCsv);
new Report(csvQuery, toPdf); // any combination, no new class

Same skeleton, same guarantee that the retry runs, and the pieces mix freely. This is Strategy — and the honest summary is that Template Method is Strategy done with inheritance, appropriate when there is exactly one axis and the steps are genuinely internal to the algorithm.