behavioral
Template Method
Fix an algorithm's skeleton in a base class; let subclasses fill in steps.
Template Method defines the skeleton of an algorithm in an operation, deferring some steps to subclasses. The base class implements the algorithm — the ordering, the invariants, the error handling — and declares one or more `protected abstract` steps that subclasses fill in. Subclasses can extend the algorithm's steps but cannot reorder them. This is the classic inheritance-based answer to 'vary part of a process' — Strategy is its composition-based counterpart.
The problem
A data ingestion service imports records from three sources: a nightly SFTP drop of CSVs, a JSON webhook, and an Excel file uploaded by admins. Each source has a different parse step and a slightly different validation step, but the workflow is identical: acquire → parse → validate → normalize → persist → emit. If each source implements the workflow independently, the ordering drifts, error handling diverges, and a new invariant — say 'commit in transaction' — has to be added to three places.
The solution
Put the workflow in an abstract `BaseImporter` with a single `import()` method that calls `acquire()`, `parse()`, `validate()`, `normalize()`, `persist()`, and `emit()`. The base class implements the orchestration (step order, transaction wrapping, retry on transient failures). Only `parse()` and `validate()` are abstract — the steps that genuinely differ per source. `CSVImporter`, `JSONImporter`, `ExcelImporter` implement those two and inherit everything else.
When to use it
- Several variants of a process share the same skeleton, differing only in a few steps.
- The ordering and invariants of the algorithm must be controlled centrally, not per subclass.
- Subclasses should not be allowed to reorder steps — the order is part of the contract.
- You want to add new invariants (transaction wrapping, retries, metrics) to all variants at once.
When not to use it
- The variants differ in the whole algorithm, not just a step — Strategy or a factory is a better fit.
- The language prefers composition over inheritance, or the base class hierarchy would grow wide.
- The 'skeleton' is one or two lines — inlining and duplicating is clearer than a base class.
- Steps need to be swapped at runtime — Template Method's variation is fixed at compile time by subclass choice.
Analogy
A recipe card for bread: mix, knead, proof, shape, bake, cool. Different breads — sourdough, ciabatta, rye — fill in their own flour mix and proofing times. The card does not change. Every baker follows the same six steps in the same order; what varies is what they put into the steps.
Diagram
Implementation
interface Record { [key: string]: unknown }
interface RawData { source: string; body: string }
abstract class BaseImporter {
async import(source: string): Promise<{ imported: number }> {
const raw = await this.acquire(source);
const records = await this.parse(raw);
const validated: Record[] = [];
for (const record of records) {
const result = this.validate(record);
if (!result.ok) {
this.onValidationError(record, result.errors);
continue;
}
validated.push(this.normalize(record));
}
await this.persist(validated);
return { imported: validated.length };
}
// Hook — subclasses may override to customize, base implementation is a no-op.
protected onValidationError(_record: Record, _errors: string[]): void {}
protected abstract acquire(source: string): Promise<RawData>;
protected abstract parse(raw: RawData): Promise<Record[]>;
// Defaults — overridable but usually inherited as-is.
protected validate(record: Record): { ok: boolean; errors: string[] } {
return { ok: true, errors: [] };
}
protected normalize(record: Record): Record {
return record;
}
protected async persist(records: Record[]): Promise<void> {
// bulk insert, transaction-wrapped
}
}
class CSVImporter extends BaseImporter {
protected async acquire(source: string): Promise<RawData> {
const res = await fetch(source);
return { source, body: await res.text() };
}
protected async parse(raw: RawData): Promise<Record[]> {
const [header, ...rows] = raw.body.split("\n");
const cols = header.split(",");
return rows.map((row) => {
const cells = row.split(",");
return Object.fromEntries(cols.map((col, i) => [col, cells[i]]));
});
}
}
class JSONImporter extends BaseImporter {
protected async acquire(source: string): Promise<RawData> {
const res = await fetch(source);
return { source, body: await res.text() };
}
protected async parse(raw: RawData): Promise<Record[]> {
return JSON.parse(raw.body);
}
protected validate(record: Record): { ok: boolean; errors: string[] } {
const errors: string[] = [];
if (typeof record.id !== "string") errors.push("id must be a string");
return { ok: errors.length === 0, errors };
}
}
// Both callers use the same import() — the workflow is inherited.
const csvResult = await new CSVImporter().import("https://example.com/data.csv");
const jsonResult = await new JSONImporter().import("https://example.com/data.json");The base class owns the sequence: acquire → parse → validate → normalize → persist, plus the per-record error handling hook. CSVImporter and JSONImporter differ only in how they acquire and parse — they cannot skip validation or persist out of order, because the base class does not expose those steps. Adding 'bulk insert in a transaction' or 'emit an audit event on every persist' means editing one method in BaseImporter and every importer gets the improvement for free.
Advantages
- The skeleton is written once and enforced — subclasses cannot reorder steps.
- Cross-cutting invariants (transactions, retries, metrics) live in one place.
- Subclasses are small — they implement only the genuinely variant steps.
- Adding a new variant is a new subclass, not a new copy of the workflow.
Disadvantages
- Uses inheritance, which is rigid — steps cannot be swapped at runtime.
- Fragile base class: adding an abstract step forces every subclass to implement it.
- The base class is not always the best place for invariants — a wrapper or Strategy is often cleaner.
- Test doubles for subclasses must satisfy the base class contract, not just the abstract methods.
Related patterns
- Strategy — Template Method varies steps via inheritance; Strategy varies the whole algorithm via composition. The choice between them is 'do I own the skeleton or the algorithm?'
- Factory Method — Factory Method is often used as one step inside a Template Method — the workflow is fixed, product construction is the varying step.
- Visitor — Both separate operations from data. Template Method fixes the sequence of steps; Visitor fixes the set of operations across a type hierarchy. They address orthogonal concerns.