Skip to content
aipatterns.dev

Design patternBehavioral Patterns

Template Method

Fix an algorithm's sequence of steps in a base class and let subclasses fill in or override individual steps without changing the order.

Complexity
Low
Seen in the wild
Medium
Also known as
Hook Method

Intent

Write the skeleton of an algorithm once, as a method that calls a sequence of steps. Subclasses supply the steps that differ and may override optional hooks, but they can't change the order or skip the invariant parts. The base class owns the "when"; subclasses own some of the "how".

Forces

  • Shared sequence vs varying details: three importers split, parse, validate and summarize in the same order, but each parses differently.
  • Duplicate code vs coupling through inheritance: copying the sequence into each variant drifts, but a base class ties every variant to its internals.
  • Invariants enforced once vs freedom for variants: validation must run for every format, so no variant should be able to forget it.
  • Optional extension points vs a readable skeleton: each hook you add is one more thing a reader has to check for overrides.

Therefore

Put the fixed sequence in one non-overridable method of a base class, and turn each varying step into an abstract method or a hook with a default.

Abstract steps are mandatory: every subclass must implement them. Hooks have a default (often empty) that a subclass may override. Mark the template method final (or its equivalent) so subclasses can't reorder the steps. In languages without implementation inheritance, such as Go, or wherever functions are values, pass the steps in as an interface or as functions instead.

Structure

Diagram source (Mermaid)
classDiagram
  class StatementImporter {
    <<abstract>>
    +importStatement(raw) ImportResult
    +splitRecords(raw) List
    +isHeader(record) bool
    +parseRecord(record) Transaction
  }
  class CommaSeparatedImporter {
    +isHeader(record) bool
    +parseRecord(record) Transaction
  }
  class FixedWidthImporter {
    +splitRecords(raw) List
    +parseRecord(record) Transaction
  }
  class ImportResult {
    -List transactions
    -List rejected
  }
  StatementImporter <|-- CommaSeparatedImporter
  StatementImporter <|-- FixedWidthImporter
  StatementImporter ..> ImportResult : builds

Participants

  • Abstract class (StatementImporter): defines the template method importStatement, the abstract steps, and hooks with default behavior.
  • Template method (importStatement): the fixed sequence. It calls the steps in order and enforces the invariants, here date validation and rejection tracking.
  • Concrete classes (CommaSeparatedImporter, FixedWidthImporter): implement the abstract steps and override only the hooks they need.

Use it when

  • Two or more classes run the same steps in the same order and differ in one or two of them. That is the Duplicate Code smell with a shared shape.
  • An invariant step, such as validation, auditing or resource cleanup, must run for every variant and must not be skippable.
  • A framework needs to call user code at fixed points of its own lifecycle, such as setup and teardown in a test runner.
  • You are already in an inheritance hierarchy and the variation is small and stable.

Do not use it when

  • The skeleton is one function's body and callers choose the steps. Pass the varying steps as function parameters (importStatement(raw, { parseRecord, isHeader })) - in a language with first-class functions this covers a one-function skeleton without a class hierarchy, and the caller can mix steps freely. The hierarchy earns its keep when subclasses override hooks between framework calls or share protected state across the skeleton.
  • Steps vary independently. Two parsing styles times two splitting styles needs four subclasses. Compose the steps instead, as Strategy objects or functions; that needs two of each.
  • Variants must change at runtime or per call. Inheritance fixes the choice when the object is created.
  • The base class and the subclasses live in different packages or teams. Every change to the base class can break subclasses it can't see (the fragile base class problem). Prefer delegation; see Replace Inheritance with Delegation.
  • There is only one variant. A plain function with no extension points is enough.

Application grill

Inspect first: the workflows duplicated across subclasses or branches, and which steps actually differ; whether the language idiom prefers step callbacks or higher-order functions (it does in TypeScript, Python and Go); the existing inheritance depth.

Missing pieces: none beyond the language; prefer a function taking step callbacks where the language allows - the class hierarchy form is for when subclasses are a domain concept, not just variation.

Ask before you build:

  • Scale: how many steps vary per implementation - one varying step is a callback, not a hierarchy?
  • Performance: virtual dispatch cost is negligible outside tight loops - is this one?
  • Reliability: when the template's step order changes, which subclasses silently break - who owns the step contract?
  • Budget: deep hierarchies are the classic maintenance tax - is this one level deep?
  • Implementation: how are hook defaults documented, and is there a test for a subclass that forgets a required step?

Implementation steps

  1. Put the duplicated methods side by side and mark which lines are identical and which differ.
  2. Split each method into the same named steps with Extract Method, so the methods differ only in step bodies. Form Template Method is the refactoring for this.
  3. Pull the now-identical sequence up into a base class as the template method, and mark it final or document that it must not be overridden.
  4. Declare the differing steps abstract. Give optional steps a default and treat them as hooks.
  5. Keep the number of abstract steps small. Each one is a method every subclass must write.
  6. Delete the duplicated sequences from the subclasses and run the tests.
  7. If a subclass overrides almost every step, stop. It doesn't share the algorithm, and it should not inherit it.

Example

A personal finance app imports bank statements. One bank exports CSV with a header row; another sends fixed-width records with a trailer line. Both must reject records with bad dates the same way.

interface Transaction { date: string; description: string; amountCents: number }
interface ImportResult { transactions: Transaction[]; rejected: string[] }

abstract class StatementImporter {
  // The template method: subclasses must not override it.
  importStatement(raw: string): ImportResult {
    const result: ImportResult = { transactions: [], rejected: [] };
    for (const record of this.splitRecords(raw)) {
      if (this.isHeader(record)) continue;
      const tx = this.parseRecord(record);
      if (tx && /^\d{4}-\d{2}-\d{2}$/.test(tx.date)) result.transactions.push(tx);
      else result.rejected.push(record);
    }
    return result;
  }

  protected abstract parseRecord(record: string): Transaction | null;

  protected splitRecords(raw: string): string[] {
    return raw.split("\n").filter((line) => line.trim().length > 0);
  }

  protected isHeader(_record: string): boolean {
    return false; // hook: most formats have no header
  }
}

class CommaSeparatedImporter extends StatementImporter {
  protected isHeader(record: string): boolean {
    return record.startsWith("date,");
  }
  protected parseRecord(record: string): Transaction | null {
    const [date, description, amount] = record.split(",");
    const cents = Math.round(Number(amount) * 100);
    return Number.isFinite(cents) ? { date, description, amountCents: cents } : null;
  }
}

class FixedWidthImporter extends StatementImporter {
  // Layout: date YYYYMMDD [0,8), description [8,24), signed cents [24,).
  protected splitRecords(raw: string): string[] {
    return super.splitRecords(raw).filter((line) => !line.startsWith("TRL"));
  }
  protected parseRecord(record: string): Transaction | null {
    const d = record.slice(0, 8);
    const cents = Number(record.slice(24).trim());
    if (!Number.isInteger(cents)) return null;
    const date = `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
    return { date, description: record.slice(8, 24).trim(), amountCents: cents };
  }
}

const csv = "date,description,amount\n2026-03-01,Coffee roaster,-18.50\n2026-03-02,Refund,abc\n2026-03-03,Payroll,2400.00\n";
const fixed = "20260301Coffee roaster  -1850\n2026-3-3Payroll         +240000\nTRL2\n";

const a = new CommaSeparatedImporter().importStatement(csv);
console.log(a.transactions.length, a.rejected); // 2 [ '2026-03-02,Refund,abc' ]
const b = new FixedWidthImporter().importStatement(fixed);
console.log(b.transactions.map((t) => t.amountCents), b.rejected.length); // [ -1850 ] 1

Trade-offs

Gains

  • The sequence and its invariants exist once. No variant can skip validation or reorder steps.
  • A new variant writes only the steps that differ.
  • Hooks give controlled extension points instead of open-ended overriding.
  • It removes duplicated control flow without changing any caller.

Costs

  • Inheritance couples subclasses to the base class's internals. Renaming or reordering a step can break every subclass.
  • The algorithm is split across files. To understand one run, a reader traces the template method and every override.
  • The choice of variant is fixed at construction and can't be combined with another variant.
  • Hooks multiply. A base class with many hooks becomes hard to reason about, and subclasses override them in surprising combinations.
  • In Go, and in any code that passes functions, the same result needs no hierarchy at all.

Relations

  • Strategy varies the whole algorithm through composition at runtime. Template Method varies parts of it through inheritance at compile time. When steps start varying independently, convert each step into a strategy.
  • Factory Method is often one step of a template method: the skeleton calls an overridable method to create an object it needs.
  • Builder with a director also runs a fixed sequence of steps, but to assemble an object, and through composition rather than subclassing.

Agent checklist

  • The variants really share the same steps in the same order; no subclass overrides most of the steps.
  • You considered a single function that takes the varying steps as parameters, and the class hierarchy is justified.
  • The template method is final, @final, or documented as not overridable.
  • Abstract steps are few, and every optional step is a hook with a safe default.
  • Invariant steps such as validation live only in the template method, not in subclasses.
  • Existing tests pass, and each concrete class has tests for its own steps.

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.