Skip to content
StackPractices
beginner By Mathias Paulenko

Template Method Pattern

Define the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the algorithm's structure. A behavioral design pattern.

Topics: design

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Template Method Pattern

Overview

The Template Method Pattern is a behavioral design pattern that defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the algorithm’s overall structure. It promotes code reuse by extracting common behavior into a template while allowing customization of individual steps.

When to Use

Use the Template Method Pattern when:

  • Multiple classes share a similar algorithm with minor variations in specific steps
  • You want to avoid code duplication by extracting the common algorithm skeleton
  • Subclasses should be able to customize certain steps without changing the overall flow
  • You need to enforce a specific sequence of operations
  • Examples: data parsers, report generators, game loops, ETL pipelines

Solution

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._notify()

    @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 _notify(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 _notify(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.notify();
  }

  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);
  }

  notify() {
    // 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 };
  }

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

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

Java

public 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);
        notify();
    }

    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 notify() {
        // Hook — subclasses may override
    }
}

public 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);
    }
}

public 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 notify() {
        System.out.println("JSON import completed!");
    }
}

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

Explanation

The Template Method Pattern has two types of methods in the base class:

  • Template Method (import_data): The public method that defines the algorithm’s skeleton. It should be final when possible to prevent accidental override.
  • Abstract/Primitive Methods (fetch, parse): Steps that must be implemented by subclasses
  • Concrete Methods (validate, save): Steps with default implementations that subclasses can inherit
  • Hook Methods (notify): Optional steps that subclasses can override but aren’t required to

Variants

VariantDescriptionUse Case
ClassicInheritance-based with abstract methodsFrameworks, parsers
Strategy-basedComposition with strategy objects injectedWhen you need runtime flexibility
Callback-basedFunctions passed as argumentsJavaScript/Node.js streams

What Works

  • Make the template method final to prevent subclasses from breaking the algorithm flow
  • Keep hooks optional — document clearly which methods are required vs. optional
  • Minimize the number of abstract methods — too many make subclasses complex
  • Document the algorithm’s steps and their invariants
  • Consider composition (Strategy) when subclasses would need to override many methods

Common Mistakes

  • Forgetting to mark the template method as final, allowing subclasses to break the algorithm
  • Making every step abstract, forcing subclasses to implement methods they don’t need
  • Using Template Method when composition (Strategy) would be more flexible
  • Introducing deep inheritance hierarchies just to vary a single step
  • Overriding concrete methods in subclasses when hooks would suffice

Frequently Asked Questions

Q: What is the difference between Template Method and Strategy? A: Template Method uses inheritance to vary parts of an algorithm. Strategy uses composition to swap entire algorithms. Prefer Strategy when you need runtime flexibility or many variations.

Q: Can I combine Template Method with Factory Method? A: Yes — very common. The template method can call a Factory Method to create objects at specific steps, letting subclasses customize which classes are instantiated.

Is this pattern suitable for small projects?

For small projects with few components, this pattern may add unnecessary complexity. Start simple and introduce the pattern when you feel the pain it solves.

How does this pattern compare to alternatives?

Each pattern makes different trade-offs. Review the variants table above and consider your specific constraints: team size, performance requirements, and future scaling plans.

Can I partially apply this pattern?

Yes. Many teams adopt patterns incrementally. Start with the core idea and add sophistication as needed. The pattern is a guide, not a strict blueprint.

Advanced Topics

Scenario: Template Method for ETL Pipeline

// Template Method: define the algorithm skeleton in the base class
abstract class ETLPipeline {
  // Template method: defines the flow, not overridden
  async run(source: DataSource): Promise<void> {
    const rawData = await this.extract(source);
    const transformed = await this.transform(rawData);
    await this.load(transformed);
    await this.notify();
  }

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

  // Hook: optional, subclasses can override
  async notify(): Promise<void> {
    console.log("[ETL] Pipeline completed");
  }
}

// Concrete ETL: 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 ETL: 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 notify(): Promise<void> {
    await slack.notify("#data", "ETL completed: API -> Redis");
  }
}

// Usage
const csvPipeline = new CSVToDBPipeline();
await csvPipeline.run({ path: "/data/users.csv" });

const apiPipeline = new APIToRedisPipeline();
await apiPipeline.run({ url: "https://api.example.com/items" });

Lessons:

  • Template Method defines the skeleton in the base class
  • Subclasses implement the steps (extract, transform, load)
  • Hooks: optional methods that subclasses can override
  • The flow (run) does not change: only the steps vary
  • Ideal for pipelines, algorithms with fixed steps and variations
  • Template Method vs Strategy: TM uses inheritance; Strategy uses composition

### Template Method vs Strategy: which do I use?

Template Method uses inheritance: the subclass implements steps of the algorithm. Strategy uses composition: the client injects the strategy. TM is more rigid: the flow is fixed in the base. Strategy is more flexible: the client changes strategy at runtime. Use TM when the algorithm flow is fixed and only steps vary. Use Strategy when you need to change the entire algorithm at runtime. For ETL with fixed steps, TM. For variable discounts, Strategy.