Design Patterns, Explained Simply · Behavioral Patterns: How Objects Share Work

Template Method

Write the skeleton of an algorithm once in a base class and let subclasses fill in specific steps, so every variant follows the same order of steps.

The problem

A data platform imports customer files: CSV from one partner, JSON from another, and fixed-width text from a bank. Each importer was written separately. All of them read the file, parse rows, validate them, save them and write an audit line, but one importer forgot validation and another saves before validating. Bugs come from the steps being in the wrong order, not from the parsing itself.

Real-world analogy

A franchise restaurant's recipe card: "1. Prepare the base. 2. Add toppings. 3. Bake for 12 minutes. 4. Slice and box." Head office fixes the steps and their order. Each restaurant can choose its own toppings (the varying step), but nobody skips baking.

Without the pattern

def import_csv(text):
    rows = parse_csv(text)
    save(rows)                 # oops: saved before validating
    validate(rows)
def import_json(text):
    rows = parse_json(text)    # forgot to validate at all
    save(rows)

The pattern

  • Abstract class with a template method (run()) that calls the steps in a fixed order. The template method itself is not meant to be overridden (final in Java).
  • Abstract steps that subclasses must implement (parse()).
  • Hooks: optional steps with a default implementation that subclasses may override (clean() that does nothing by default).
  • Concrete classes (CsvImporter, JsonImporter): implement the varying steps only.

Template Method uses inheritance to vary steps. Strategy uses composition to vary a whole algorithm. Modern code often prefers Strategy, but Template Method is everywhere in frameworks.

The code

One import skeleton, different parsing stepsTry it yourself
class Importer:
    def run(self, text):  # the template method: fixed order of steps
        rows = self.parse(text)
        rows = self.clean(rows)
        valid = [r for r in rows if r["email"] and "@" in r["email"]]
        print(f"{type(self).__name__}: {len(valid)} saved, {len(rows) - len(valid)} rejected")

    def parse(self, text):  # abstract step: subclasses must implement it
        raise NotImplementedError

    def clean(self, rows):  # hook: optional, does nothing by default
        return rows


class CsvImporter(Importer):
    def parse(self, text):
        rows = []
        for line in text.strip().split("\n")[1:]:  # skip the header
            name, email = line.split(",")
            rows.append({"name": name, "email": email})
        return rows


class LegacyBankImporter(Importer):
    def parse(self, text):  # fixed-width: name in 10 chars, then email
        return [{"name": line[:10], "email": line[10:]} for line in text.strip().split("\n")]

    def clean(self, rows):  # this source pads fields with spaces
        return [{k: v.strip() for k, v in r.items()} for r in rows]


CsvImporter().run("name,email\nAda,[email protected]\nBob,\nCy,[email protected]")
LegacyBankImporter().run("Dee       [email protected]  \nEve       not-an-email     ")

Output

CsvImporter: 2 saved, 1 rejected
LegacyBankImporter: 1 saved, 1 rejected

Both importers validate before saving because neither one controls the order; the base class does. The bank importer used the optional clean() hook; the CSV importer didn't need it.

When to use it

  • Several classes run the same steps in the same order and differ only in some steps.
  • You are writing a framework or base class and want users to customise specific steps without breaking the overall flow.
  • You want to enforce invariants like "always validate before saving".

When not to use it

  • Variants differ in many steps. Subclasses become hard to follow when half the logic is in the base class and half in the subclass.
  • You need to combine variations (CSV parsing + bank cleaning). Inheritance can't mix them; Strategy (passing a parser and a cleaner) can.
  • Deep inheritance chains of template methods ("yo-yo problem": reading jumps up and down the hierarchy).

You've already used it

  • Test frameworks: setUp() / tearDown() in unittest, JUnit and PHPUnit are hooks around a fixed test-running skeleton.
  • Web frameworks: Django's class-based views (get_queryset(), get_context_data()), React class components' lifecycle methods, Laravel's FormRequest (rules(), authorize()).
  • Java: AbstractList implements most list methods on top of the abstract get() and size().

Key points

  • Template Method fixes an algorithm's skeleton in a base class; subclasses fill in steps.
  • Abstract steps must be implemented; hooks are optional overrides.
  • It enforces step order, but relies on inheritance. Prefer Strategy when you need to mix variations.
  • Further reading: Refactoring Guru: Template Method.

Practice questions

What is the difference between an abstract step and a hook in Template Method?

Answer

An abstract step must be implemented by every subclass (parse()). A hook has a default (often empty) implementation in the base class, and subclasses may override it if they need to (clean(), setUp()).

How does Template Method differ from Strategy?

Answer

Template Method uses inheritance: the base class fixes the algorithm and subclasses override steps. Strategy uses composition: the whole algorithm (or a step) is a separate object passed in. Strategy is more flexible for combining variations; Template Method is convenient for frameworks.

Give an example of Template Method in a testing framework.

Answer

xUnit frameworks (unittest, JUnit, PHPUnit) run each test as a fixed skeleton: setUp(), the test method, then tearDown(). You override the hooks; the framework controls the order.

Open in the app to mark lessons complete, take notes and track your progress.