Template Method Pattern

advanced 4-7 YOE lld design-pattern behavioral

The Template Method pattern defines the skeleton of an algorithm in a base class. The overall structure stays the same, but subclasses can override specific steps without changing the algorithm’s flow.

Think of it like a recipe template. Every cake follows the same steps: prepare batter → bake → decorate. But a chocolate cake uses cocoa in the batter, a vanilla cake uses vanilla extract, and a red velvet cake adds food coloring. The template (steps and order) stays the same. The specifics change.

The Problem

Let’s say we’re building data parsers for different file formats — CSV, JSON, and XML. Each parser does the same high-level thing:

  1. Open and read the file
  2. Parse the raw data into records
  3. Validate the records
  4. Process/transform the data
  5. Generate output

Without Template Method, we’d duplicate steps 1, 3, 4, and 5 across all three parsers. Only step 2 (parsing) actually differs. That’s a lot of repeated code. And if we need to change the validation logic, we’d have to update it in three places.

How It Works

Template Method Structure
AbstractClass (DataParser)
+ process()  ← template method (final)
    1. readFile()  ← concrete step
    2. parseData()  ← abstract step
    3. validate()  ← concrete step
    4. beforeOutput()  ← hook (optional)
    5. output()  ← concrete step
│ subclasses override abstract & hook methods
CSVParser
overrides parseData()
JSONParser
overrides parseData()
XMLParser
overrides parseData()
+ beforeOutput()

There are two types of steps subclasses can customize:

  • Abstract methods (mandatory) — subclasses MUST override these. The base class has no default implementation.
  • Hook methods (optional) — subclasses CAN override these. The base class provides a default (usually empty) implementation.

The template method itself should not be overridden. In Java we’d make it final. In Python we rely on convention.

Implementation — Data Parser

from abc import ABC, abstractmethod

class DataParser(ABC):
    def process(self, filepath: str):
        """Template method -- defines the algorithm skeleton."""
        raw_data = self.read_file(filepath)
        records = self.parse_data(raw_data)
        valid_records = self.validate(records)
        self.before_output(valid_records)  # hook
        self.output(valid_records)

    def read_file(self, filepath: str) -> str:
        print(f"Reading file: {filepath}")
        # In real code, we'd actually read the file
        return f"raw content of {filepath}"

    @abstractmethod
    def parse_data(self, raw_data: str) -> list[dict]:
        """Subclasses MUST implement this."""
        pass

    def validate(self, records: list[dict]) -> list[dict]:
        print(f"Validating {len(records)} records...")
        return [r for r in records if r]  # filter out empty records

    def before_output(self, records: list[dict]):
        """Hook -- subclasses CAN override this. Does nothing by default."""
        pass

    def output(self, records: list[dict]):
        print(f"Output: {len(records)} records processed.")

class CSVParser(DataParser):
    def parse_data(self, raw_data: str) -> list[dict]:
        print("Parsing CSV: splitting by commas and newlines...")
        return [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]

class JSONParser(DataParser):
    def parse_data(self, raw_data: str) -> list[dict]:
        print("Parsing JSON: using json.loads()...")
        return [{"name": "Charlie", "role": "dev"}]

class XMLParser(DataParser):
    def parse_data(self, raw_data: str) -> list[dict]:
        print("Parsing XML: walking DOM tree...")
        return [{"node": "item", "value": "data"}]

    def before_output(self, records: list[dict]):
        print("XML-specific: converting attributes to flat keys...")

# Usage
csv_parser = CSVParser()
csv_parser.process("data.csv")
# Reading file: data.csv
# Parsing CSV: splitting by commas and newlines...
# Validating 2 records...
# Output: 2 records processed.

print("---")
xml_parser = XMLParser()
xml_parser.process("data.xml")
# Reading file: data.xml
# Parsing XML: walking DOM tree...
# Validating 1 records...
# XML-specific: converting attributes to flat keys...
# Output: 1 records processed.
class DataParser {
  process(filepath) {
    const rawData = this.readFile(filepath);
    const records = this.parseData(rawData);
    const valid = this.validate(records);
    this.beforeOutput(valid); // hook
    this.output(valid);
  }

  readFile(filepath) {
    console.log(`Reading file: ${filepath}`);
    return `raw content of ${filepath}`;
  }

  parseData(rawData) {
    throw new Error("Subclass must implement parseData()");
  }

  validate(records) {
    console.log(`Validating ${records.length} records...`);
    return records.filter((r) => r !== null);
  }

  beforeOutput(records) {
    // Hook -- does nothing by default. Override if needed.
  }

  output(records) {
    console.log(`Output: ${records.length} records processed.`);
  }
}

class CSVParser extends DataParser {
  parseData(rawData) {
    console.log("Parsing CSV: splitting by commas and newlines...");
    return [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }];
  }
}

class JSONParser extends DataParser {
  parseData(rawData) {
    console.log("Parsing JSON: using JSON.parse()...");
    return [{ name: "Charlie", role: "dev" }];
  }
}

class XMLParser extends DataParser {
  parseData(rawData) {
    console.log("Parsing XML: walking DOM tree...");
    return [{ node: "item", value: "data" }];
  }

  beforeOutput(records) {
    console.log("XML-specific: converting attributes to flat keys...");
  }
}

// Usage
const csvParser = new CSVParser();
csvParser.process("data.csv");

console.log("---");
const xmlParser = new XMLParser();
xmlParser.process("data.xml");
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

abstract class DataParser {
    // Template method -- final so subclasses can't change the flow
    public final void process(String filepath) {
        String rawData = readFile(filepath);
        List<String> records = parseData(rawData);
        List<String> valid = validate(records);
        beforeOutput(valid);  // hook
        output(valid);
    }

    private String readFile(String filepath) {
        System.out.println("Reading file: " + filepath);
        return "raw content of " + filepath;
    }

    // Abstract -- subclasses MUST implement
    protected abstract List<String> parseData(String rawData);

    private List<String> validate(List<String> records) {
        System.out.println("Validating " + records.size() + " records...");
        return records.stream()
            .filter(r -> r != null && !r.isEmpty())
            .collect(Collectors.toList());
    }

    // Hook -- subclasses CAN override
    protected void beforeOutput(List<String> records) { }

    private void output(List<String> records) {
        System.out.println("Output: " + records.size() + " records processed.");
    }
}

class CSVParser extends DataParser {
    protected List<String> parseData(String rawData) {
        System.out.println("Parsing CSV: splitting by commas...");
        return List.of("Alice,30", "Bob,25");
    }
}

class XMLParser extends DataParser {
    protected List<String> parseData(String rawData) {
        System.out.println("Parsing XML: walking DOM tree...");
        return List.of("<item>data</item>");
    }

    @Override
    protected void beforeOutput(List<String> records) {
        System.out.println("XML-specific: converting attributes...");
    }
}

Hooks vs Abstract Methods

This distinction comes up a lot in interviews:

Abstract MethodHook Method
Must override?YesNo
Has default?NoYes (usually empty)
PurposeStep that MUST be customizedOptional customization point
ExampleparseData()beforeOutput()

Hooks give subclasses a chance to react at certain points in the algorithm without forcing them to. It’s a nice touch that makes the pattern more flexible.

When to Use

  • Data processing pipelines — ETL jobs, file converters, report generators
  • Game loops — initialize → update → render, but each game customizes the steps
  • Test frameworks — setUp → runTest → tearDown (JUnit, pytest fixtures)
  • Web frameworks — request lifecycle hooks (before/after middleware)

When NOT to Use

  • When the algorithm has too many steps that ALL need overriding — at that point, we’re not reusing anything
  • When subclasses need to change the order of steps — Template Method locks the order
  • When composition would work better — sometimes Strategy is a cleaner fit because it doesn’t require inheritance

Template Method vs Strategy

Both let us vary parts of an algorithm. The difference is HOW:

  • Template Method uses inheritance. The base class defines the flow, subclasses fill in the blanks.
  • Strategy uses composition. The algorithm is a separate object we plug in.

Template Method is great when the overall flow is fixed and only a few steps vary. Strategy is better when we need to swap the entire algorithm at runtime.

In simple language, Template Method is like a fill-in-the-blank form. The structure is already printed. We just fill in our specific answers. The form decides the order of questions. We decide the answers. No one can rearrange the questions — and that’s the whole point.