# 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.

- kind: pattern
- category: behavioral
- aliases: Hook Method
- canonical: https://aipatterns.dev/design-patterns/template-method
- inspired-by: https://refactoring.guru/design-patterns/template-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

```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](https://aipatterns.dev/refactoring/smells/duplicate-code.md) 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](https://aipatterns.dev/design-patterns/strategy.md) 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](https://aipatterns.dev/refactoring/techniques/replace-inheritance-with-delegation.md).
- 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](https://aipatterns.dev/refactoring/techniques/extract-method.md), so the methods differ only in step bodies. [Form Template Method](https://aipatterns.dev/refactoring/techniques/form-template-method.md) 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.

```ts title="importer.ts"
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
```

```python title="importer.py"
import re
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import final

ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")

@dataclass(frozen=True)
class Transaction:
    date: str
    description: str
    amount_cents: int

@dataclass
class ImportResult:
    transactions: list[Transaction] = field(default_factory=list)
    rejected: list[str] = field(default_factory=list)

class StatementImporter(ABC):
    @final
    def import_statement(self, raw: str) -> ImportResult:
        result = ImportResult()
        for record in self.split_records(raw):
            if self.is_header(record):
                continue
            tx = self.parse_record(record)
            if tx and ISO_DATE.match(tx.date):
                result.transactions.append(tx)
            else:
                result.rejected.append(record)
        return result

    @abstractmethod
    def parse_record(self, record: str) -> Transaction | None: ...

    def split_records(self, raw: str) -> list[str]:
        return [line for line in raw.split("\n") if line.strip()]

    def is_header(self, record: str) -> bool:
        return False  # hook: most formats have no header

class CommaSeparatedImporter(StatementImporter):
    def is_header(self, record: str) -> bool:
        return record.startswith("date,")

    def parse_record(self, record: str) -> Transaction | None:
        try:
            date, description, amount = record.split(",")
            return Transaction(date, description, round(float(amount) * 100))
        except ValueError:
            return None

class FixedWidthImporter(StatementImporter):
    # Layout: date YYYYMMDD [0,8), description [8,24), signed cents [24,).
    def split_records(self, raw: str) -> list[str]:
        return [r for r in super().split_records(raw) if not r.startswith("TRL")]

    def parse_record(self, record: str) -> Transaction | None:
        d, amount = record[:8], record[24:].strip()
        if not amount.lstrip("+-").isdigit():
            return None
        return Transaction(f"{d[:4]}-{d[4:6]}-{d[6:8]}", record[8:24].strip(), int(amount))

if __name__ == "__main__":
    csv = "date,description,amount\n2026-03-01,Coffee roaster,-18.50\n2026-03-02,Refund,abc\n2026-03-03,Payroll,2400.00\n"
    fixed = "20260301Coffee roaster  -1850\n2026-3-3Payroll         +240000\nTRL2\n"
    a = CommaSeparatedImporter().import_statement(csv)
    print(len(a.transactions), a.rejected)  # 2 ['2026-03-02,Refund,abc']
    b = FixedWidthImporter().import_statement(fixed)
    print([t.amount_cents for t in b.transactions], len(b.rejected))  # [-1850] 1
```

```go title="importer.go"
package main

import (
	"fmt"
	"math"
	"regexp"
	"strconv"
	"strings"
)

type Transaction struct {
	Date, Description string
	AmountCents       int
}

type ImportResult struct {
	Transactions []Transaction
	Rejected     []string
}

// Go has no inheritance: the required step is an interface,
// and optional hooks are smaller interfaces checked at runtime.
type RecordParser interface {
	ParseRecord(record string) (Transaction, bool)
}
type HeaderSkipper interface{ IsHeader(record string) bool }
type RecordFilter interface{ KeepRecord(record string) bool }

var isoDate = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)

// ImportStatement is the template: the sequence lives here, once.
func ImportStatement(p RecordParser, raw string) ImportResult {
	var result ImportResult
	for _, record := range strings.Split(raw, "\n") {
		if strings.TrimSpace(record) == "" {
			continue
		}
		if f, ok := p.(RecordFilter); ok && !f.KeepRecord(record) {
			continue
		}
		if h, ok := p.(HeaderSkipper); ok && h.IsHeader(record) {
			continue
		}
		if tx, ok := p.ParseRecord(record); ok && isoDate.MatchString(tx.Date) {
			result.Transactions = append(result.Transactions, tx)
		} else {
			result.Rejected = append(result.Rejected, record)
		}
	}
	return result
}

type CommaSeparated struct{}

func (CommaSeparated) IsHeader(r string) bool { return strings.HasPrefix(r, "date,") }
func (CommaSeparated) ParseRecord(r string) (Transaction, bool) {
	parts := strings.Split(r, ",")
	if len(parts) != 3 {
		return Transaction{}, false
	}
	amount, err := strconv.ParseFloat(parts[2], 64)
	return Transaction{parts[0], parts[1], int(math.Round(amount * 100))}, err == nil
}

// FixedWidth layout: date YYYYMMDD [0,8), description [8,24), signed cents [24,).
type FixedWidth struct{}

func (FixedWidth) KeepRecord(r string) bool { return !strings.HasPrefix(r, "TRL") }
func (FixedWidth) ParseRecord(r string) (Transaction, bool) {
	if len(r) < 25 {
		return Transaction{}, false
	}
	cents, err := strconv.Atoi(strings.TrimSpace(r[24:]))
	date := r[0:4] + "-" + r[4:6] + "-" + r[6:8]
	return Transaction{date, strings.TrimSpace(r[8:24]), cents}, err == nil
}

func main() {
	csv := "date,description,amount\n2026-03-01,Coffee roaster,-18.50\n2026-03-02,Refund,abc\n2026-03-03,Payroll,2400.00\n"
	fixed := "20260301Coffee roaster  -1850\n2026-3-3Payroll         +240000\nTRL2\n"
	a := ImportStatement(CommaSeparated{}, csv)
	fmt.Println(len(a.Transactions), a.Rejected) // 2 [2026-03-02,Refund,abc]
	b := ImportStatement(FixedWidth{}, fixed)
	fmt.Println(b.Transactions[0].AmountCents, len(b.Rejected)) // -1850 1
}
```

```java title="Importer.java"
import java.util.*;
import java.util.regex.Pattern;

public class Importer {
    record Transaction(String date, String description, long amountCents) {}
    record ImportResult(List<Transaction> transactions, List<String> rejected) {}

    abstract static class StatementImporter {
        private static final Pattern ISO_DATE = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");

        // final: subclasses fill in steps but cannot reorder them.
        public final ImportResult importStatement(String raw) {
            var result = new ImportResult(new ArrayList<>(), new ArrayList<>());
            for (String record : splitRecords(raw)) {
                if (isHeader(record)) continue;
                Optional<Transaction> tx = parseRecord(record);
                if (tx.isPresent() && ISO_DATE.matcher(tx.get().date()).matches()) {
                    result.transactions().add(tx.get());
                } else {
                    result.rejected().add(record);
                }
            }
            return result;
        }

        protected abstract Optional<Transaction> parseRecord(String record);

        protected List<String> splitRecords(String raw) {
            return raw.lines().filter(l -> !l.isBlank()).toList();
        }

        protected boolean isHeader(String record) { return false; } // hook
    }

    static final class CommaSeparatedImporter extends StatementImporter {
        @Override protected boolean isHeader(String r) { return r.startsWith("date,"); }

        @Override protected Optional<Transaction> parseRecord(String r) {
            String[] p = r.split(",");
            try {
                return Optional.of(new Transaction(p[0], p[1], Math.round(Double.parseDouble(p[2]) * 100)));
            } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
                return Optional.empty();
            }
        }
    }

    // Layout: date YYYYMMDD [0,8), description [8,24), signed cents [24,).
    static final class FixedWidthImporter extends StatementImporter {
        @Override protected List<String> splitRecords(String raw) {
            return super.splitRecords(raw).stream().filter(l -> !l.startsWith("TRL")).toList();
        }

        @Override protected Optional<Transaction> parseRecord(String r) {
            try {
                long cents = Long.parseLong(r.substring(24).trim());
                String date = r.substring(0, 4) + "-" + r.substring(4, 6) + "-" + r.substring(6, 8);
                return Optional.of(new Transaction(date, r.substring(8, 24).trim(), cents));
            } catch (NumberFormatException | StringIndexOutOfBoundsException e) {
                return Optional.empty();
            }
        }
    }

    public static void main(String[] args) {
        String csv = "date,description,amount\n2026-03-01,Coffee roaster,-18.50\n2026-03-02,Refund,abc\n2026-03-03,Payroll,2400.00\n";
        String fixed = "20260301Coffee roaster  -1850\n2026-3-3Payroll         +240000\nTRL2\n";
        var a = new CommaSeparatedImporter().importStatement(csv);
        System.out.println(a.transactions().size() + " " + a.rejected()); // 2 [2026-03-02,Refund,abc]
        var b = new FixedWidthImporter().importStatement(fixed);
        System.out.println(b.transactions().get(0).amountCents() + " " + b.rejected().size()); // -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](https://aipatterns.dev/design-patterns/strategy.md) 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](https://aipatterns.dev/design-patterns/factory-method.md) is often one step of a template method: the skeleton calls an overridable method to create an object it needs.
- [Builder](https://aipatterns.dev/design-patterns/builder.md) 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.
