StackPractices
beginner By Mathias Paulenko

Template Method Pattern

Define an algorithm's skeleton in a base class and let subclasses fill in the steps. The GoF behavioral pattern for removing duplicated pipeline logic.

Topics: design

Overview

You’ve got three importers — CSV, JSON, XML — and each one runs the same pipeline: fetch, parse, validate, save, notify. The sequence never changes, only the steps do. Then someone fixes a validation bug in CSVImporter and forgets the other two, and now the same algorithm has three slightly different implementations to audit.

The Template Method Pattern — one of the 11 behavioral patterns in the Gang of Four’s Design Patterns (Gamma et al., 1994) — moves the algorithm’s skeleton into a base class. The base class owns the sequence as a single final method; subclasses override only the steps that genuinely vary. Fix validate once and every importer gets the fix.

When to Use

Use the Template Method Pattern when:

  • Several classes run the same algorithm with small variations in individual steps — data parsers, report generators, ETL pipelines, game loops.
  • You want the sequence of operations enforced, not just suggested. A final template method makes the order impossible to break from a subclass.
  • Some steps have sensible defaults and others must be supplied per implementation — the pattern gives you three member kinds to express exactly that.

Don’t reach for it when:

  • Only one step varies and a parameter or a callback would do. A function argument is lighter than an inheritance relationship.
  • Steps need to vary independently at runtime — that’s composition, and Strategy is the right tool.
  • You already have a deep hierarchy. Adding Template Method on top of three levels of inheritance makes the “which class provides this step?” question painful to answer.

Problem

This is what the pattern removes: the same orchestration duplicated in every concrete class.

class CSVImporter:
    def import_data(self, source):
        raw = self.fetch(source)
        parsed = self.parse(raw)
        if "id" not in parsed:
            raise ValueError("Missing required field: id")
        print(f"Saving: {parsed}")

class JSONImporter:
    def import_data(self, source):
        raw = self.fetch(source)          # identical
        parsed = self.parse(raw)
        if "id" not in parsed:            # identical
            raise ValueError("Missing required field: id")
        print(f"Saving: {parsed}")        # identical
        print("JSON import completed!")   # the only real difference

Every duplicated line is a place where the copies can drift. Add a dry_run flag or a retry step and you touch N classes; change the validation rule and you need to check they all stayed in sync.

Solution

Pull the skeleton up. The base class defines import_data once and declares which steps subclasses must supply, which they may override, and which are optional hooks.

Template Method class diagram — abstract DataImporter with final importData calling fetch, parse, validate, save, onComplete; CSVImporter and JSONImporter subclass it and implement fetch and parse

Python

from abc import ABC, abstractmethod

class DataImporter(ABC):
    def import_data(self, source: str):
        """The template method defining the algorithm skeleton."""
        raw = self._fetch(source)
        parsed = self._parse(raw)
        validated = self._validate(parsed)
        self._save(validated)
        self._on_complete()

    @abstractmethod
    def _fetch(self, source: str) -> str:
        pass

    @abstractmethod
    def _parse(self, raw: str) -> dict:
        pass

    def _validate(self, data: dict) -> dict:
        """Default step; can be overridden."""
        if "id" not in data:
            raise ValueError("Missing required field: id")
        return data

    def _save(self, data: dict):
        """Default step; can be overridden."""
        print(f"Saving: {data}")

    def _on_complete(self):
        """Hook method — subclasses may override or ignore."""
        pass

class CSVImporter(DataImporter):
    def _fetch(self, source: str) -> str:
        return f"CSV content from {source}"

    def _parse(self, raw: str) -> dict:
        return {"id": 1, "format": "csv", "content": raw}

class JSONImporter(DataImporter):
    def _fetch(self, source: str) -> str:
        return f"JSON content from {source}"

    def _parse(self, raw: str) -> dict:
        return {"id": 2, "format": "json", "content": raw}

    def _on_complete(self):
        print("JSON import completed!")

# Usage
CSVImporter().import_data("users.csv")
JSONImporter().import_data("users.json")

JavaScript

class DataImporter {
  importData(source) {
    const raw = this.fetch(source);
    const parsed = this.parse(raw);
    const validated = this.validate(parsed);
    this.save(validated);
    this.onComplete();
  }

  fetch(source) {
    throw new Error("Subclasses must implement fetch()");
  }

  parse(raw) {
    throw new Error("Subclasses must implement parse()");
  }

  validate(data) {
    if (!data.id) throw new Error("Missing required field: id");
    return data;
  }

  save(data) {
    console.log("Saving:", data);
  }

  onComplete() {
    // Hook — subclasses may override
  }
}

class CSVImporter extends DataImporter {
  fetch(source) {
    return `CSV content from ${source}`;
  }

  parse(raw) {
    return { id: 1, format: "csv", content: raw };
  }
}

class JSONImporter extends DataImporter {
  fetch(source) {
    return `JSON content from ${source}`;
  }

  parse(raw) {
    return { id: 2, format: "json", content: raw };
  }

  onComplete() {
    console.log("JSON import completed!");
  }
}

// Usage
new CSVImporter().importData("users.csv");
new JSONImporter().importData("users.json");

Java

import java.util.Map;

abstract class DataImporter {
    public final void importData(String source) {
        String raw = fetch(source);
        Map<String, Object> parsed = parse(raw);
        Map<String, Object> validated = validate(parsed);
        save(validated);
        onComplete();
    }

    protected abstract String fetch(String source);
    protected abstract Map<String, Object> parse(String raw);

    protected Map<String, Object> validate(Map<String, Object> data) {
        if (!data.containsKey("id")) {
            throw new IllegalArgumentException("Missing required field: id");
        }
        return data;
    }

    protected void save(Map<String, Object> data) {
        System.out.println("Saving: " + data);
    }

    protected void onComplete() {
        // Hook — subclasses may override
    }
}

class CSVImporter extends DataImporter {
    protected String fetch(String source) {
        return "CSV content from " + source;
    }

    protected Map<String, Object> parse(String raw) {
        return Map.of("id", 1, "format", "csv", "content", raw);
    }
}

class JSONImporter extends DataImporter {
    protected String fetch(String source) {
        return "JSON content from " + source;
    }

    protected Map<String, Object> parse(String raw) {
        return Map.of("id", 2, "format", "json", "content", raw);
    }

    protected void onComplete() {
        System.out.println("JSON import completed!");
    }
}

Two details worth noting in the Java version: importData is final so no subclass can reorder the steps, and the hook is named onComplete rather than notify — Object.notify() is final and any method with that name won’t compile. Each class would normally live in its own file; they’re grouped here for readability.

Explanation

The base class ends up with four kinds of members:

  • Template method (importData): the public entry point that fixes the sequence. Mark it final (or the language equivalent) so the skeleton can’t be broken accidentally.
  • Abstract steps (fetch, parse): no default exists, so every subclass must provide them. Keep this list short — each one is a tax on every future subclass.
  • Concrete steps (validate, save): working defaults a subclass inherits for free and overrides only when it needs different behavior.
  • Hooks (onComplete): empty or near-empty extension points. A subclass plugs in only if it cares; the pipeline calls it either way.

The trade-off is real: the pattern buys a single, enforced sequence at the price of an inheritance relationship. Subclasses become tightly coupled to the base class’s structure — rename a step or change its signature and every subclass must move. This is the classic fragile base class problem, and it’s why the GoF book itself leans toward composition for bigger variations. Template Method shines when the steps are few and stable; when the variation is the algorithm itself, prefer Strategy.

Variants

VariantDescriptionUse Case
ClassicInheritance with abstract and hook methodsFrameworks, parsers, importers
Callback-basedSteps injected as functions or lambdasJavaScript/Node streams, when subclassing is overhead
Hollywood-style frameworksThe framework calls your steps — “don’t call us, we’ll call you”Test runners, build plugins, servlet lifecycles

The callback-based variant keeps the same idea — a fixed sequence with variable steps — but swaps the base class for function parameters. In TypeScript, an options object with fetch, parse, and optional onComplete callbacks covers most cases where a full class hierarchy would be ceremony.

What Works

  • Make the template method final — the whole point is that the sequence isn’t negotiable.
  • Keep hooks optional and empty by default — document clearly which methods are required vs. optional, or subclasses will guess.
  • Minimize abstract steps — every abstract method is mandatory work for every future subclass; two or three is a healthy number.
  • Name hooks for what happened, not what to do — onComplete, afterSave read like the extension points they’re (and avoid colliding with inherited methods like Java’s Object.notify()).
  • Consider Strategy when subclasses would override many steps — if half the algorithm varies, inheritance is the wrong vehicle.

Common Mistakes

  • Forgetting to mark the template method final, letting a subclass quietly reorder or skip steps.
  • Making every step abstract, which forces subclasses to implement steps they don’t need — the opposite of the pattern’s convenience.
  • Calling overridable steps from the constructor. In Java and C# the subclass fields aren’t initialized yet when the base constructor runs, so the overridden step sees half-built state.
  • Stuffing real logic into hooks. A hook that does meaningful work is a hidden required step; make it abstract or accept that subclasses forget to call super.
  • Introducing a second level of abstract classes to vary just one step — that’s when the hierarchy tax exceeds the duplication it was meant to remove.

Advanced Topics

Scenario: Template Method for an ETL Pipeline

// Template Method: the base class owns the pipeline shape
abstract class ETLPipeline {
  async run(source: DataSource): Promise<void> {
    const rawData = await this.extract(source);
    const transformed = await this.transform(rawData);
    await this.load(transformed);
    await this.onComplete();
  }

  abstract extract(source: DataSource): Promise<unknown[]>;
  abstract transform(data: unknown[]): Promise<unknown[]>;
  abstract load(data: unknown[]): Promise<void>;

  async onComplete(): Promise<void> {
    console.log("[ETL] Pipeline completed");
  }
}

// Concrete pipeline: CSV -> DB
class CSVToDBPipeline extends ETLPipeline {
  async extract(source: DataSource): Promise<unknown[]> {
    const csv = await readFile(source.path);
    return parseCSV(csv);
  }
  async transform(data: unknown[]): Promise<unknown[]> {
    return data.map(row => ({
      ...row,
      normalizedEmail: (row.email as string).toLowerCase().trim(),
      timestamp: new Date().toISOString(),
    }));
  }
  async load(data: unknown[]): Promise<void> {
    await db.batchInsert("users", data);
  }
}

// Concrete pipeline: API -> Redis
class APIToRedisPipeline extends ETLPipeline {
  async extract(source: DataSource): Promise<unknown[]> {
    const response = await fetch(source.url);
    return response.json();
  }
  async transform(data: unknown[]): Promise<unknown[]> {
    return data.filter(item => item.active);
  }
  async load(data: unknown[]): Promise<void> {
    const pipeline = redis.pipeline();
    data.forEach(item => pipeline.set(`item:${item.id}`, JSON.stringify(item)));
    await pipeline.exec();
  }
  async onComplete(): Promise<void> {
    await slack.notify("#data", "ETL completed: API -> Redis");
  }
}

What this version shows that the basic one doesn’t:

  • The template method run is async and untouched by subclasses — extraction, transformation and load always happen in that order.
  • Only the steps change between pipelines: a filesystem read vs. an HTTP fetch, a normalization vs. a filter, a batch insert vs. a Redis pipeline.
  • onComplete does nothing in one pipeline and sends a Slack notification in the other — that asymmetry is exactly what hooks are for.
  • Adding a third pipeline (say, S3 to Elasticsearch) means writing three step implementations, not copying the orchestration.

See Also

  • Strategy Pattern — the composition-based sibling: pick it when the whole algorithm varies, not just the steps
  • Factory Pattern — pairs naturally with Template Method when a step must decide which class to instantiate
  • Design Patterns Guide — the full catalog: when each GoF pattern earns its complexity
  • Companion repository — the importer example as runnable Python, JavaScript, and Java files

Further Reading

Frequently Asked Questions

What is the difference between Template Method and Strategy?

Template Method varies the steps of an algorithm through inheritance; the flow stays fixed in the base class. Strategy swaps the whole algorithm through composition, and the client picks which one to inject. Template Method fits a pipeline with fixed steps like ETL; Strategy fits cases where the algorithm itself changes at runtime, like pricing rules or sorting orders.

Can I combine Template Method with Factory Method?

Yes, and it's common. A step inside the template can be a Factory Method that decides which concrete class to instantiate — the template controls when objects get created, and the subclass controls what gets created.

What happens if a subclass forgets to implement an abstract step?

It depends on the language. Java refuses to compile — abstract methods must be implemented. Python's abc raises TypeError at instantiation, not at import. JavaScript has no abstract keyword; the common convention is throwing inside the base method, which only fails when the step is actually called, so bugs surface later.

Should the template method be final?

Yes, whenever the language allows it. The template method is the contract: if a subclass can override it, the invariant "these steps run in this order" stops being a guarantee and becomes a suggestion. Java has final, C# requires sealing, Python and JavaScript rely on convention — document it and don't touch it in subclasses.

How do I test subclasses without re-testing the skeleton?

Test the steps in isolation on each subclass (does CSVImporter.parse handle quoted fields?) and test the sequence once on the base class with a spy or fake subclass recording the call order. If you find yourself testing the same five-step sequence in every subclass test, that's the duplication the pattern was supposed to remove.

When does Template Method become a liability?

When the variation grows beyond steps. If subclasses start overriding concrete methods wholesale, adding mid-sequence steps, or skipping the template entirely for edge cases, the algorithm is no longer "fixed skeleton plus variations" — that's the signal to move to composition with Strategy or a chain of handlers.