Skip to content
Commitwise
Menu
Design Patterns

Template Method

Define the fixed steps of an algorithm once in a base class and let subclasses fill in only the parts that vary, protect the template from being overridden, use hooks for optional steps, and know when functions or Strategy are the better choice, in Swift, Kotlin, Java and TypeScript.

intermediate

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

The app imports contacts from CSV files, from JSON exports and from a partner’s XML feed. Each importer was written by a different developer, at a different time: the CSV one validates emails, the JSON one doesn’t, the XML one writes to the database one row at a time and takes a minute. They all do the same four things (read, check, save, report) and each does them slightly differently. A bug fixed in one is still there in the other two.

The Template Method pattern writes those four steps once, in a base class, and lets each importer supply only the step that actually differs: how to read its format.

New to some of these? Open what you need, or skip ahead.

Basics:What are inheritance and overriding?

A class can inherit from another: the subclass gets all of the base class’s methods and can override some of them with its own version.

Kotlin:  open class Importer { open fun parse(...) }     class CsvImporter : Importer() { override fun parse(...) }
Swift:   class Importer { func parse(...) }              class CsvImporter: Importer { override func parse(...) }
Java:    abstract class Importer { abstract parse(...) }  class CsvImporter extends Importer { parse(...) }

When code calls parse on an Importer that’s really a CsvImporter, the CSV version runs. The Classes, inheritance and interfaces tutorial covers it.

Part Not started

A fixed skeleton with replaceable steps

A cake recipe card at a bakery says: mix the base, add the flavor, bake for 30 minutes, let it cool, add the topping. The steps and their order never change; each cake fills in only the two blanks. A new baker can’t forget to let the cake cool, because cooling isn’t their job; it’s on the card.

In code, the card is a method in a base class (the template method) that calls the steps in order. The blanks are methods the subclass must provide. The subclass doesn’t decide the order, and it can’t skip a step.

Template Method defines an algorithm’s skeleton in a base class operation, deferring some steps to subclasses via primitive operations (abstract, must be implemented) and hooks (overridable with a default, often empty). The base class calls the subclass (“Hollywood principle”: don’t call us, we’ll call you). It’s inheritance-based inversion of control, so it’s well suited when the variants share real behavior and invariants and are few, and poorly suited when variants need to be combined or swapped at runtime (that’s Strategy). The template itself must not be overridable: final in Java and Swift classes, non-open in Kotlin; TypeScript has no final, so conventions or composition must stand in.

struct ContactRow { let name: String; let email: String }
struct ImportReport { let imported: Int; let rejected: Int }

class ContactImporter {
    private let store: ContactStore
    init(store: ContactStore) { self.store = store }

    // The template method: final, so subclasses can't change the steps or their order
    final func run(_ data: Data) throws -> ImportReport {
        let rows = try parse(data)                         // varies: each format
        let valid = rows.filter(isValid)                   // fixed, with a hook below
        try store.saveAll(valid)                           // fixed: one batch, one transaction
        didImport(valid)                                   // hook: optional
        return ImportReport(imported: valid.count, rejected: rows.count - valid.count)
    }

    // Must be overridden (Swift has no abstract methods)
    func parse(_ data: Data) throws -> [ContactRow] { fatalError("Subclasses implement parse(_:)") }

    // Hook with a default
    func didImport(_ rows: [ContactRow]) {}

    private func isValid(_ row: ContactRow) -> Bool {
        !row.name.isEmpty && row.email.contains("@")
    }
}

final class CsvContactImporter: ContactImporter {
    override func parse(_ data: Data) throws -> [ContactRow] {
        String(decoding: data, as: UTF8.self)
            .split(separator: "\n").dropFirst()             // skip the header line
            .map { $0.split(separator: ",", omittingEmptySubsequences: false).map(String.init) }
            .map { ContactRow(name: $0[0], email: $0[1]) }
    }
}
data class ContactRow(val name: String, val email: String)
data class ImportReport(val imported: Int, val rejected: Int)

abstract class ContactImporter(private val store: ContactStore) {

    // The template method: not open, so subclasses can't change the steps or their order
    fun run(data: ByteArray): ImportReport {
        val rows = parse(data)                             // varies: each format
        val valid = rows.filter(::isValid)                 // fixed
        store.saveAll(valid)                               // fixed: one batch, one transaction
        didImport(valid)                                   // hook: optional
        return ImportReport(imported = valid.size, rejected = rows.size - valid.size)
    }

    protected abstract fun parse(data: ByteArray): List<ContactRow>

    protected open fun didImport(rows: List<ContactRow>) {}

    private fun isValid(row: ContactRow) = row.name.isNotBlank() && "@" in row.email
}

class CsvContactImporter(store: ContactStore) : ContactImporter(store) {
    override fun parse(data: ByteArray): List<ContactRow> =
        data.decodeToString().lines().drop(1).filter { it.isNotBlank() }
            .map { it.split(",") }
            .map { ContactRow(name = it[0], email = it[1]) }
}
public record ContactRow(String name, String email) {}
public record ImportReport(int imported, int rejected) {}

public abstract class ContactImporter {
    private final ContactStore store;
    protected ContactImporter(ContactStore store) { this.store = store; }

    // The template method: final, so subclasses can't change the steps or their order
    public final ImportReport run(byte[] data) {
        List<ContactRow> rows = parse(data);                         // varies: each format
        List<ContactRow> valid = rows.stream().filter(this::isValid).toList();   // fixed
        store.saveAll(valid);                                        // fixed: one batch
        didImport(valid);                                            // hook: optional
        return new ImportReport(valid.size(), rows.size() - valid.size());
    }

    protected abstract List<ContactRow> parse(byte[] data);

    protected void didImport(List<ContactRow> rows) {}

    private boolean isValid(ContactRow row) {
        return !row.name().isBlank() && row.email().contains("@");
    }
}

public final class CsvContactImporter extends ContactImporter {
    public CsvContactImporter(ContactStore store) { super(store); }

    @Override
    protected List<ContactRow> parse(byte[] data) {
        return new String(data, StandardCharsets.UTF_8).lines().skip(1)
                .filter(line -> !line.isBlank())
                .map(line -> line.split(",", -1))
                .map(cols -> new ContactRow(cols[0], cols[1]))
                .toList();
    }
}
interface ContactRow { name: string; email: string }
interface ImportReport { imported: number; rejected: number }

abstract class ContactImporter {
  constructor(private readonly store: ContactStore) {}

  // The template method. TypeScript has no "final": don't override run() in subclasses
  async run(data: string): Promise<ImportReport> {
    const rows = this.parse(data);                       // varies: each format
    const valid = rows.filter((r) => this.isValid(r));   // fixed
    await this.store.saveAll(valid);                     // fixed: one batch
    this.didImport(valid);                               // hook: optional
    return { imported: valid.length, rejected: rows.length - valid.length };
  }

  protected abstract parse(data: string): ContactRow[];

  protected didImport(_rows: ContactRow[]): void {}

  private isValid(row: ContactRow): boolean {
    return row.name.trim() !== "" && row.email.includes("@");
  }
}

class CsvContactImporter extends ContactImporter {
  protected parse(data: string): ContactRow[] {
    return data.split("\n").slice(1).filter((l) => l.trim() !== "")
      .map((l) => l.split(","))
      .map(([name, email]) => ({ name, email }));
  }
}
  • run is the template: parse, validate, save, notify, report. It’s final (or not open in Kotlin), so no importer can reorder or skip the steps.
  • parse is the only thing each format must write. A JSON or XML importer is a few lines.
  • didImport is a hook: an optional step with a default that does nothing. An importer that wants to send a “done” notification overrides it; the others ignore it.
  • Validation and saving are written once, so every format checks emails and saves in one batch. A fix there fixes all importers.
Spot the bugThe importer that skipped validation

Tap the line with the bug.

class ContactImporter {
    func run(_ data: Data) throws -> ImportReport {
        let rows = try parse(data)
        let valid = rows.filter(isValid)
        try store.saveAll(valid)
        return ImportReport(imported: valid.count, rejected: rows.count - valid.count)
    }
}

final class PartnerXmlImporter: ContactImporter {
    override func run(_ data: Data) throws -> ImportReport {
        let rows = try XmlContacts.decode(data)
        try store.saveAll(rows)
        return ImportReport(imported: rows.count, rejected: 0)
    }
}
abstract class ContactImporter(protected val store: ContactStore) {
    open fun run(data: ByteArray): ImportReport {
        val rows = parse(data)
        val valid = rows.filter(::isValid)
        store.saveAll(valid)
        return ImportReport(valid.size, rows.size - valid.size)
    }
}

class PartnerXmlImporter(store: ContactStore) : ContactImporter(store) {
    override fun run(data: ByteArray): ImportReport {
        val rows = XmlContacts.decode(data)
        store.saveAll(rows)
        return ImportReport(rows.size, 0)
    }
}
public abstract class ContactImporter {
    public ImportReport run(byte[] data) {
        List<ContactRow> rows = parse(data);
        List<ContactRow> valid = rows.stream().filter(this::isValid).toList();
        store.saveAll(valid);
        return new ImportReport(valid.size(), rows.size() - valid.size());
    }
}

public final class PartnerXmlImporter extends ContactImporter {
    @Override public ImportReport run(byte[] data) {
        List<ContactRow> rows = XmlContacts.decode(data);
        store.saveAll(rows);
        return new ImportReport(rows.size(), 0);
    }
}
abstract class ContactImporter {
  async run(data: string): Promise<ImportReport> {
    const rows = this.parse(data);
    const valid = rows.filter((r) => this.isValid(r));
    await this.store.saveAll(valid);
    return { imported: valid.length, rejected: rows.length - valid.length };
  }
}

class PartnerXmlImporter extends ContactImporter {
  async run(data: string): Promise<ImportReport> {
    const rows = XmlContacts.decode(data);
    await this.store.saveAll(rows);
    return { imported: rows.length, rejected: 0 };
  }
}

Predict the output

JsonContactImporter overrides only parse and the didImport hook (to log 'done'). A file has 10 rows, 2 without an email. In which order do things happen when run is called, and what's the report?

  • Base-class skeleton (the template method, non-overridable) calls primitive operations (abstract) and hooks (default no-op).
  • Inversion of control via inheritance: the base calls the subclass.
  • Lock the template: final (Java, Swift classes), non-open (Kotlin); TypeScript lacks final, so prefer composition or lint rules.
  • Good for a few variants sharing real invariants; poor for runtime swapping or combining variants.

Check this part

  1. In Template Method, who decides the order of the steps?

  2. What is a hook?

  3. Why should the template method be final?

The base class has one method that runs the steps in a fixed order (the template method) and calls abstract methods for the steps that vary. Subclasses implement only those steps; they can't change the order or skip steps. Optional steps are hooks with a default that does nothing.

Part Not started

Template methods you already override

You’ve used this pattern many times without the name. When you override viewDidLoad() in a UIViewController, onCreate() in an Android Activity, or setUp() in a test class, you’re filling in a step of a template the framework owns. UIKit decides when viewDidLoad runs; Android decides when onCreate runs; the test runner calls setUp before every test. You only write what happens in the step.

That’s why the documentation insists on calling super.onCreate(savedInstanceState) or super.viewDidLoad(): the base class’s version of the step does work the rest of the template depends on.

Frameworks are largely built from template methods: UIViewController and Activity lifecycles, RecyclerView.Adapter, Spring’s OncePerRequestFilter.doFilterInternal, AbstractRoutingDataSource, JUnit’s lifecycle, React class components. The trade-offs show up there too: the fragile base class problem (a framework change to the skeleton changes every subclass’s behavior), required super calls that nothing enforces except lint (@CallSuper, NS_REQUIRES_SUPER), and initialization order hazards when a base class constructor calls overridable methods.

Production puzzles

Predict the output

In Kotlin, Java or TypeScript: a base class constructor calls an overridable method createClient(). A subclass overrides it to build a client from its own baseUrl property, initialized in its declaration (val baseUrl = 'https://api.example.com'). What does createClient see when the subclass is constructed?

Swift note

In Swift, a template defined in a protocol extension behaves differently from one in a class: a method that’s only in the extension (not declared in the protocol) is statically dispatched. If a conforming type “overrides” such a step, the template still calls the extension’s version. Declare every step the template calls as a protocol requirement, so conforming types’ implementations are used.

When to prefer composition

Template Method fixes the variation point at compile time and couples subclasses to the base class’s internals. Prefer passing the varying step as a function or a Strategy object when:

  • variants need to be combined (a CSV parser with strict validation, a JSON parser with lenient validation): inheritance gives you one axis, composition gives you any number;
  • the step must change at runtime (user setting, feature flag, A/B test);
  • the base class would be a library class others extend, making every change to its skeleton a breaking one.
struct ContactImporter {
    let store: ContactStore
    let parse: (Data) throws -> [ContactRow]
    func run(_ data: Data) throws -> ImportReport { /* same steps, calling parse(data) */ }
}
let csv = ContactImporter(store: store, parse: CsvParser.parse)
class ContactImporter(
    private val store: ContactStore,
    private val parse: (ByteArray) -> List<ContactRow>,
) {
    fun run(data: ByteArray): ImportReport { /* same steps, calling parse(data) */ }
}
val csv = ContactImporter(store, CsvParser::parse)
public final class ContactImporter {
    private final ContactStore store;
    private final Function<byte[], List<ContactRow>> parse;
    public ContactImporter(ContactStore store, Function<byte[], List<ContactRow>> parse) {
        this.store = store; this.parse = parse;
    }
    public ImportReport run(byte[] data) { /* same steps, calling parse.apply(data) */ }
}
var csv = new ContactImporter(store, CsvParser::parse);
class ContactImporter {
  constructor(private readonly store: ContactStore, private readonly parse: (data: string) => ContactRow[]) {}
  async run(data: string): Promise<ImportReport> { /* same steps, calling this.parse(data) */ }
}
const csv = new ContactImporter(store, CsvParser.parse);

The skeleton is still written once and still can’t be skipped, but the class is final, the parser is replaceable in tests without subclassing, and new combinations need no new classes.

Exerciseeasy

A report template

Three reports (daily sales, weekly signups, monthly refunds) all: load rows for a date range, compute a total, render a header, the rows and a footer with the total, and email the result. Only loading the rows and the title differ. Sketch the base class: which method is the template, which are abstract, and which could be hooks?

›

List the steps in order, then mark each one as “same for all” or “differs”. The template calls them all; only the differing ones are abstract.

SolutionTry it yourself first
  • Template (final): send(range): rows = loadRows(range), total = rows.sum(amount), html = render(title(), rows, total), email(html).
  • Abstract: loadRows(range) and title(): the only parts that differ.
  • Fixed (private): computing the total, rendering, emailing.
  • Hooks: filter(row) returning true by default (the refunds report could exclude test orders), and recipients() with a default list that a report may override.

If later each report needs to choose its renderer (HTML or PDF) independently of what it loads, that’s a second axis of variation: pass the renderer in, rather than creating HtmlDailySalesReport, PdfDailySalesReport and so on.

  • Framework lifecycles are template methods (viewDidLoad, onCreate, doFilterInternal, test lifecycle); super calls enforced only by lint (@CallSuper, NS_REQUIRES_SUPER).
  • Never call overridable methods from constructors (Kotlin/Java/TS see uninitialized subclass state; Swift forbids it).
  • Swift protocol extensions: non-requirement methods are statically dispatched; declare template steps as requirements.
  • Prefer functions/Strategy when variants combine, change at runtime, or the base would be a public library class.

Check this part

  1. Why does Android say to call super.onCreate() when you override onCreate?

  2. Why not call an overridable method from a base class constructor?

  3. When is passing a function better than Template Method?

Framework lifecycle methods are template methods: viewDidLoad, onCreate, setUp, doFilterInternal. You override steps; the framework owns the sequence. That's also why calling super, and not calling overridable methods from constructors, matter.

Key takeaways

  • A template method runs fixed steps in a fixed order and lets subclasses fill in only the steps that differ.
  • Make the template final, so no subclass can skip or reorder steps.
  • Hooks are optional steps with a default.
  • Lifecycle methods like viewDidLoad and onCreate are template-method steps you override.
  • Primitive operations and hooks; Hollywood principle; lock the skeleton.
  • Beware fragile base classes, missing super calls and overridable calls from constructors.
  • Swift: protocol-extension templates need every step declared as a requirement.
  • Switch to functions or Strategy for runtime choice or several axes of variation.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.