Template Method Pattern

Introduction

The Template Method Pattern defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the algorithm’s overall structure. The base class controls the workflow — the “what” and “when” — while subclasses provide the “how” for individual steps.

This pattern is everywhere in frameworks. Every time you extend a base class and override a hook method (like setUp() in a test framework or doGet() in a servlet), you are using the Template Method Pattern.

The Problem

You are building a data export system. Users can export records as CSV, JSON, or XML. Each export follows the same high-level workflow: fetch data, validate it, transform it into the target format, and write the output. The naive approach is to implement three separate export classes, each duplicating the fetch and validation logic.

This leads to code duplication. When the validation rules change, you must update three classes. When you add a new export format, you copy-paste an existing class and modify the format-specific parts — a recipe for bugs and inconsistency.

The Solution

The Template Method Pattern pulls the shared workflow into a base class method (the “template method”) that calls abstract steps for the parts that vary. The base class handles fetching and validation, while subclasses implement only the format-specific transformation and writing logic.

The template method is typically marked as final (in Java) to prevent subclasses from altering the workflow order. Subclasses can customize individual steps but cannot skip validation or reorder the pipeline.

Key Principle

The Hollywood Principle: “Don’t call us, we’ll call you.” The base class controls the flow and calls subclass methods at the right time. Subclasses do not drive the algorithm — they fill in the blanks.

Java Example

Scenario: A data export pipeline supporting CSV, JSON, and XML formats.

import java.util.List;
import java.util.Map;

// Abstract base class with template method
public abstract class DataExporter {

    // Template method — defines the algorithm skeleton
    public final void export(List<Map<String, String>> records) {
        validate(records);
        String formatted = transform(records);
        String fileName = getFileName();
        writeOutput(fileName, formatted);
        System.out.println("Export complete: " + fileName + "\n");
    }

    // Shared step — same for all formats
    private void validate(List<Map<String, String>> records) {
        if (records == null || records.isEmpty()) {
            throw new IllegalArgumentException("No records to export");
        }
        System.out.println("Validated " + records.size() + " records.");
    }

    // Abstract steps — subclasses provide the implementation
    protected abstract String transform(List<Map<String, String>> records);
    protected abstract String getFileName();

    // Hook method — default implementation, can be overridden
    protected void writeOutput(String fileName, String content) {
        System.out.println("Writing to " + fileName
            + " (" + content.length() + " chars)");
    }
}

// Concrete: CSV Exporter
public class CsvExporter extends DataExporter {

    @Override
    protected String transform(List<Map<String, String>> records) {
        StringBuilder sb = new StringBuilder();
        // Header row
        sb.append(String.join(",", records.get(0).keySet())).append("\n");
        // Data rows
        for (Map<String, String> record : records) {
            sb.append(String.join(",", record.values())).append("\n");
        }
        return sb.toString();
    }

    @Override
    protected String getFileName() {
        return "export.csv";
    }
}

// Concrete: JSON Exporter
public class JsonExporter extends DataExporter {

    @Override
    protected String transform(List<Map<String, String>> records) {
        StringBuilder sb = new StringBuilder("[\n");
        for (int i = 0; i < records.size(); i++) {
            sb.append("  {");
            Map<String, String> record = records.get(i);
            int j = 0;
            for (Map.Entry<String, String> entry : record.entrySet()) {
                sb.append("\"").append(entry.getKey()).append("\": \"")
                  .append(entry.getValue()).append("\"");
                if (++j < record.size()) sb.append(", ");
            }
            sb.append("}");
            if (i < records.size() - 1) sb.append(",");
            sb.append("\n");
        }
        sb.append("]");
        return sb.toString();
    }

    @Override
    protected String getFileName() {
        return "export.json";
    }
}

// Concrete: XML Exporter
public class XmlExporter extends DataExporter {

    @Override
    protected String transform(List<Map<String, String>> records) {
        StringBuilder sb = new StringBuilder("<records>\n");
        for (Map<String, String> record : records) {
            sb.append("  <record>\n");
            for (Map.Entry<String, String> entry : record.entrySet()) {
                sb.append("    <").append(entry.getKey()).append(">")
                  .append(entry.getValue())
                  .append("</").append(entry.getKey()).append(">\n");
            }
            sb.append("  </record>\n");
        }
        sb.append("</records>");
        return sb.toString();
    }

    @Override
    protected String getFileName() {
        return "export.xml";
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        List<Map<String, String>> records = List.of(
            Map.of("name", "Alice", "role", "Engineer"),
            Map.of("name", "Bob", "role", "Designer")
        );

        new CsvExporter().export(records);
        new JsonExporter().export(records);
        new XmlExporter().export(records);
    }
}

Python Example

Same data export pipeline in Python.

from abc import ABC, abstractmethod


class DataExporter(ABC):
    """Abstract base class with template method."""

    def export(self, records: list[dict[str, str]]) -> None:
        """Template method — defines the algorithm skeleton."""
        self._validate(records)
        formatted = self.transform(records)
        file_name = self.get_file_name()
        self.write_output(file_name, formatted)
        print(f"Export complete: {file_name}\n")

    def _validate(self, records: list[dict[str, str]]) -> None:
        """Shared step — same for all formats."""
        if not records:
            raise ValueError("No records to export")
        print(f"Validated {len(records)} records.")

    @abstractmethod
    def transform(self, records: list[dict[str, str]]) -> str:
        """Subclasses provide format-specific transformation."""
        pass

    @abstractmethod
    def get_file_name(self) -> str:
        pass

    def write_output(self, file_name: str, content: str) -> None:
        """Hook method — default implementation, can be overridden."""
        print(f"Writing to {file_name} ({len(content)} chars)")


class CsvExporter(DataExporter):

    def transform(self, records: list[dict[str, str]]) -> str:
        headers = ",".join(records[0].keys())
        rows = [",".join(record.values()) for record in records]
        return headers + "\n" + "\n".join(rows)

    def get_file_name(self) -> str:
        return "export.csv"


class JsonExporter(DataExporter):

    def transform(self, records: list[dict[str, str]]) -> str:
        import json
        return json.dumps(records, indent=2)

    def get_file_name(self) -> str:
        return "export.json"


class XmlExporter(DataExporter):

    def transform(self, records: list[dict[str, str]]) -> str:
        lines = ["<records>"]
        for record in records:
            lines.append("  <record>")
            for key, value in record.items():
                lines.append(f"    <{key}>{value}</{key}>")
            lines.append("  </record>")
        lines.append("</records>")
        return "\n".join(lines)

    def get_file_name(self) -> str:
        return "export.xml"


# Usage
if __name__ == "__main__":
    records = [
        {"name": "Alice", "role": "Engineer"},
        {"name": "Bob", "role": "Designer"},
    ]

    CsvExporter().export(records)
    JsonExporter().export(records)
    XmlExporter().export(records)

When to Use

  • Shared workflow, varying steps — When multiple classes follow the same algorithm but differ in specific steps (e.g., different export formats, different report generators).
  • Enforcing a sequence — When you want to guarantee that steps execute in a specific order (validation before transformation, setup before execution).
  • Reducing code duplication — When subclasses share significant logic and only a few steps differ between them.
  • Framework extension points — When building a framework where users override hooks without altering the core workflow.

Real-World Usage

  • Java Servlet HttpServlet — The service() method is the template that dispatches to doGet(), doPost(), etc.
  • JUnit / TestNG — The test lifecycle (setUp(), test method, tearDown()) is a template method pattern.
  • Spring JdbcTemplate — Handles connection management and error handling while letting you provide the query-specific logic.
  • Python unittest.TestCase — The setUp() / test_*() / tearDown() lifecycle follows the template method pattern.



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *