The template method pattern is a behavioral design pattern that defines the skeleton of an algorithm in a base class and defers some steps to subclasses. The base class owns the overall sequence, calling the steps in a fixed order while subclasses fill in the parts that vary. Subclasses never reorder the flow; they plug into hooks the template method invokes.
The problem it solves
Several workflows share the same high-level steps but differ in a few details. Exporting data might always open a stream, write a header, write rows, write a footer, and close, but the row format changes between CSV and JSON. Test runners follow setUp, run, tearDown for every case, but each test class implements run differently.
Copy-pasting the shared sequence into every variant duplicates structure and invites drift: one exporter forgets to close the stream, another writes the footer before the rows. Inlining everything into one class with conditionals grows brittle as variants multiply.
The template method pattern keeps one authoritative sequence in the base class. Shared steps stay concrete there; varying steps become overridable methods subclasses implement once.
Structure
- Abstract class, declares the template method and defines the fixed algorithm steps.
- Template method, the public method that runs the algorithm skeleton and calls hook methods in order.
- Primitive operations, abstract or overridable methods subclasses customize (required steps or optional hooks).
- Concrete classes, supply the varying steps while inheriting the shared flow.
flowchart TB Client["Client"] -->|"calls export()"| AbstractExporter["DataExporter (Abstract)"] AbstractExporter -->|"1. open()"| Open["open(), concrete"] AbstractExporter -->|"2. writeHeader()"| Header["writeHeader(), hook"] AbstractExporter -->|"3. writeRows()"| Rows["writeRows(), uses formatRow()"] AbstractExporter -->|"4. writeFooter()"| Footer["writeFooter(), hook"] AbstractExporter -->|"5. close()"| Close["close(), concrete"] CsvExporter["CsvExporter"] -.->|"extends"| AbstractExporter JsonExporter["JsonExporter"] -.->|"extends"| AbstractExporter CsvExporter -->|"overrides formatRow()"| Rows JsonExporter -->|"overrides formatRow()"| Rows
In code
A DataExporter base class defines export() as the template method. Subclasses override formatRow() and may optionally customize header or footer hooks.
// Abstract class: owns the algorithm skeleton
class DataExporter {
export(rows) {
this.open();
this.writeHeader();
this.writeRows(rows);
this.writeFooter();
return this.close();
}
open() {
this._parts = [];
}
writeHeader() {
// hook: default does nothing
}
writeRows(rows) {
for (const row of rows) {
this._parts.push(this.formatRow(row));
}
}
writeFooter() {
// hook: default does nothing
}
close() {
return this._parts.join("\n");
}
formatRow(row) {
throw new Error("formatRow() must be implemented");
}
}
class CsvExporter extends DataExporter {
writeHeader() {
this._parts.push("name,amount");
}
formatRow(row) {
return `${row.name},${row.amount}`;
}
}
class JsonExporter extends DataExporter {
open() {
super.open();
this._parts.push("[");
}
formatRow(row) {
return JSON.stringify(row);
}
writeFooter() {
this._parts.push("]");
}
close() {
return this._parts.join(",\n");
}
}
const rows = [{ name: "widget", amount: 9.99 }];
console.log(new CsvExporter().export(rows));
// name,amount
// widget,9.99type SaleRow = { name: string; amount: number };
// Abstract class: owns the algorithm skeleton
abstract class DataExporter {
protected parts: string[] = [];
export(rows: SaleRow[]): string {
this.open();
this.writeHeader();
this.writeRows(rows);
this.writeFooter();
return this.close();
}
protected open(): void {
this.parts = [];
}
protected writeHeader(): void {
// hook: default does nothing
}
protected writeRows(rows: SaleRow[]): void {
for (const row of rows) {
this.parts.push(this.formatRow(row));
}
}
protected writeFooter(): void {
// hook: default does nothing
}
protected close(): string {
return this.parts.join("\n");
}
protected abstract formatRow(row: SaleRow): string;
}
class CsvExporter extends DataExporter {
protected writeHeader(): void {
this.parts.push("name,amount");
}
protected formatRow(row: SaleRow): string {
return `${row.name},${row.amount}`;
}
}
class JsonExporter extends DataExporter {
protected open(): void {
super.open();
this.parts.push("[");
}
protected formatRow(row: SaleRow): string {
return JSON.stringify(row);
}
protected writeFooter(): void {
this.parts.push("]");
}
protected close(): string {
return this.parts.join(",\n");
}
}
const rows: SaleRow[] = [{ name: "widget", amount: 9.99 }];
console.log(new CsvExporter().export(rows));
// name,amount
// widget,9.99public record SaleRow(string Name, decimal Amount);
// Abstract class: owns the algorithm skeleton
public abstract class DataExporter
{
protected readonly List<string> Parts = new();
public string Export(IEnumerable<SaleRow> rows)
{
Open();
WriteHeader();
WriteRows(rows);
WriteFooter();
return Close();
}
protected virtual void Open() => Parts.Clear();
protected virtual void WriteHeader() { }
protected void WriteRows(IEnumerable<SaleRow> rows)
{
foreach (var row in rows)
Parts.Add(FormatRow(row));
}
protected virtual void WriteFooter() { }
protected virtual string Close() => string.Join("\n", Parts);
protected abstract string FormatRow(SaleRow row);
}
public class CsvExporter : DataExporter
{
protected override void WriteHeader() => Parts.Add("name,amount");
protected override string FormatRow(SaleRow row) => $"{row.Name},{row.Amount}";
}
public class JsonExporter : DataExporter
{
protected override void Open()
{
base.Open();
Parts.Add("[");
}
protected override string FormatRow(SaleRow row) => System.Text.Json.JsonSerializer.Serialize(row);
protected override void WriteFooter() => Parts.Add("]");
protected override string Close() => string.Join(",\n", Parts);
}
var rows = new[] { new SaleRow("widget", 9.99m) };
Console.WriteLine(new CsvExporter().Export(rows));
// name,amount
// widget,9.99from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class SaleRow:
name: str
amount: float
# Abstract class: owns the algorithm skeleton
class DataExporter(ABC):
def __init__(self) -> None:
self._parts: list[str] = []
def export(self, rows: list[SaleRow]) -> str:
self.open()
self.write_header()
self.write_rows(rows)
self.write_footer()
return self.close()
def open(self) -> None:
self._parts = []
def write_header(self) -> None:
pass # hook: default does nothing
def write_rows(self, rows: list[SaleRow]) -> None:
for row in rows:
self._parts.append(self.format_row(row))
def write_footer(self) -> None:
pass # hook: default does nothing
def close(self) -> str:
return "\n".join(self._parts)
@abstractmethod
def format_row(self, row: SaleRow) -> str: ...
class CsvExporter(DataExporter):
def write_header(self) -> None:
self._parts.append("name,amount")
def format_row(self, row: SaleRow) -> str:
return f"{row.name},{row.amount}"
class JsonExporter(DataExporter):
def open(self) -> None:
super().open()
self._parts.append("[")
def format_row(self, row: SaleRow) -> str:
import json
return json.dumps({"name": row.name, "amount": row.amount})
def write_footer(self) -> None:
self._parts.append("]")
def close(self) -> str:
return ",\n".join(self._parts)
rows = [SaleRow("widget", 9.99)]
print(CsvExporter().export(rows))
# name,amount
# widget,9.99Hooks vs abstract steps
Not every customizable step is required. Template methods often mix:
- Abstract operations, subclasses must implement them (
formatRowin the example). Omitting one is a compile-time or runtime error. - Hook methods, empty or default implementations in the base class (
writeHeader,writeFooter). Subclasses override only when needed. - Concrete steps, fully defined in the base class (
writeRows,open). Subclasses inherit them unchanged unless they deliberately callsuperand extend behavior, asJsonExporterdoes foropen()andclose().
Keeping hooks optional avoids forcing every subclass to implement steps it does not need. Marking critical steps abstract prevents silent omissions.
Relationship to SOLID
Template method applies the SOLID principles. It supports the open/closed principle because new export formats arrive as new subclasses without editing the template method. It also embodies the Hollywood principle (“don’t call us, we’ll call you”): subclasses supply steps, but the base class controls when they run.
Template method vs alternatives
- Strategy pattern, varies the whole algorithm through composition and runtime injection. Template method varies steps through inheritance and fixes the call order in the base class. Prefer strategy when algorithms are fully swappable at runtime; prefer template method when many subclasses share an identical sequence with small differences.
- Factory Method pattern, a creator class defines a workflow that calls an overridable factory method to obtain a product. Factory Method is creational; template method is behavioral. They often appear together when the skeleton both builds and uses objects.
- Plain inheritance without a template method, subclasses override public methods freely and may skip steps. A named template method makes the invariant sequence explicit and harder to break accidentally.
- Callbacks or pipeline functions, in functional code, an array of step functions achieves a similar skeleton without a class hierarchy. Reach for template method when the shared flow, default hooks, and extension points belong naturally in an object model.
Trade-offs
- Ties variants to inheritance. Deep hierarchies can become rigid; languages without good abstract-class support lean toward strategy or composition instead.
- The base class is a bottleneck: changing the template method affects every subclass. Get the invariant steps right before many clients depend on them.
- Subclasses that override concrete steps must remember to call
superwhen extending rather than replacing behavior, or the shared flow breaks silently. - Best when several implementations follow the same ordered workflow, the shared steps are stable, and only a few points vary, such as exporters, parsers, test lifecycles, or framework extension points.
