Template Method Pattern: Algorithm Skeleton

The Template Method pattern is a behavioral design pattern that defines the skeleton of an algorithm in a base class, deferring some steps to subclasses. The base class controls the overall flow—the...

Key Insights

  • The Template Method pattern defines an algorithm’s skeleton in a base class while letting subclasses customize specific steps, eliminating code duplication without sacrificing flexibility.
  • Use abstract methods for steps that must vary between implementations and hook methods for optional customization points with sensible defaults.
  • Prefer Template Method over Strategy when the algorithm structure is fixed and only certain steps need variation; choose Strategy when you need runtime flexibility or want to avoid inheritance hierarchies.

What is the Template Method Pattern?

The Template Method pattern is a behavioral design pattern that defines the skeleton of an algorithm in a base class, deferring some steps to subclasses. The base class controls the overall flow—the “what” and “when”—while subclasses provide the “how” for specific operations.

This pattern appears everywhere: framework lifecycle methods, data processing pipelines, and test fixtures all use template methods. If you’ve ever extended a class and overridden specific methods while the parent controlled the execution order, you’ve used this pattern.

from abc import ABC, abstractmethod

class DataProcessor(ABC):
    def process(self):
        """Template method - defines the algorithm skeleton."""
        self.load_data()
        self.validate()
        self.transform()
        self.save()
        self.notify()  # Hook method with default implementation
    
    @abstractmethod
    def load_data(self):
        """Primitive operation - must be implemented."""
        pass
    
    @abstractmethod
    def transform(self):
        """Primitive operation - must be implemented."""
        pass
    
    @abstractmethod
    def save(self):
        """Primitive operation - must be implemented."""
        pass
    
    def validate(self):
        """Hook method - can be overridden."""
        pass
    
    def notify(self):
        """Hook method with default behavior."""
        print("Processing complete")

The Problem It Solves

Without Template Method, similar algorithms lead to duplicated code scattered across classes. Each implementation repeats the same structure with minor variations, creating maintenance nightmares.

Consider three export classes before applying the pattern:

# Before: Duplicated algorithm structure
class CSVExporter:
    def export(self, data, filename):
        # Validate - duplicated
        if not data:
            raise ValueError("No data")
        
        # Format as CSV
        output = "\n".join(",".join(str(v) for v in row) for row in data)
        
        # Write file - duplicated
        with open(filename, 'w') as f:
            f.write(output)
        
        # Log - duplicated
        print(f"Exported to {filename}")

class JSONExporter:
    def export(self, data, filename):
        # Validate - duplicated
        if not data:
            raise ValueError("No data")
        
        # Format as JSON
        import json
        output = json.dumps(data, indent=2)
        
        # Write file - duplicated
        with open(filename, 'w') as f:
            f.write(output)
        
        # Log - duplicated
        print(f"Exported to {filename}")

The validation, file writing, and logging are identical. Only the formatting differs. When requirements change—say, adding compression or changing the logging format—you must update every class.

Pattern Structure and Components

The Template Method pattern has three key components:

AbstractClass: Contains the template method and defines primitive operations. The template method calls operations in a specific order, establishing the algorithm’s invariant parts.

ConcreteClass: Implements the primitive operations to carry out subclass-specific steps.

Hook Methods: Optional extension points with default (often empty) implementations that subclasses can override.

from abc import ABC, abstractmethod
from typing import Any, List

class DataExporter(ABC):
    """AbstractClass: Defines template method and primitive operations."""
    
    def export(self, data: List[Any], filename: str) -> None:
        """
        Template method - final algorithm structure.
        Subclasses cannot override this method.
        """
        self._validate(data)
        formatted = self._format(data)
        self._before_write(filename)  # Hook
        self._write(formatted, filename)
        self._after_write(filename)   # Hook
    
    def _validate(self, data: List[Any]) -> None:
        """Concrete operation - same for all subclasses."""
        if not data:
            raise ValueError("Cannot export empty data")
    
    @abstractmethod
    def _format(self, data: List[Any]) -> str:
        """Primitive operation - must be implemented by subclasses."""
        pass
    
    def _write(self, content: str, filename: str) -> None:
        """Concrete operation - same for all subclasses."""
        with open(filename, 'w') as f:
            f.write(content)
    
    def _before_write(self, filename: str) -> None:
        """Hook method - optional, empty default."""
        pass
    
    def _after_write(self, filename: str) -> None:
        """Hook method - optional, has default behavior."""
        print(f"Export complete: {filename}")

Implementation Walkthrough

Let’s build a complete data export system with CSV, JSON, and XML exporters:

import json
from typing import Any, List, Dict

class CSVExporter(DataExporter):
    """ConcreteClass: CSV implementation."""
    
    def __init__(self, delimiter: str = ","):
        self.delimiter = delimiter
    
    def _format(self, data: List[Dict[str, Any]]) -> str:
        if not data:
            return ""
        
        headers = list(data[0].keys())
        lines = [self.delimiter.join(headers)]
        
        for row in data:
            values = [str(row.get(h, "")) for h in headers]
            lines.append(self.delimiter.join(values))
        
        return "\n".join(lines)
    
    def _before_write(self, filename: str) -> None:
        print(f"Preparing CSV export to {filename}")


class JSONExporter(DataExporter):
    """ConcreteClass: JSON implementation."""
    
    def __init__(self, indent: int = 2):
        self.indent = indent
    
    def _format(self, data: List[Any]) -> str:
        return json.dumps(data, indent=self.indent)


class XMLExporter(DataExporter):
    """ConcreteClass: XML implementation."""
    
    def __init__(self, root_element: str = "data"):
        self.root_element = root_element
    
    def _format(self, data: List[Dict[str, Any]]) -> str:
        lines = [f'<?xml version="1.0" encoding="UTF-8"?>']
        lines.append(f"<{self.root_element}>")
        
        for item in data:
            lines.append("  <record>")
            for key, value in item.items():
                lines.append(f"    <{key}>{value}</{key}>")
            lines.append("  </record>")
        
        lines.append(f"</{self.root_element}>")
        return "\n".join(lines)
    
    def _after_write(self, filename: str) -> None:
        # Override hook to add XML-specific behavior
        print(f"XML export complete: {filename}")
        print("Validating XML structure...")


# Usage
data = [
    {"name": "Alice", "age": 30, "city": "Seattle"},
    {"name": "Bob", "age": 25, "city": "Portland"},
]

csv_exporter = CSVExporter()
csv_exporter.export(data, "users.csv")

json_exporter = JSONExporter(indent=4)
json_exporter.export(data, "users.json")

xml_exporter = XMLExporter(root_element="users")
xml_exporter.export(data, "users.xml")

Each exporter shares the same algorithm structure but provides its own formatting logic. Adding a new format requires only implementing _format() and optionally overriding hooks.

Hooks vs. Abstract Methods

The distinction between hooks and abstract methods is crucial for good Template Method design.

Abstract methods are mandatory extension points. Subclasses must provide an implementation. Use them when there’s no sensible default and the step is essential to the algorithm.

Hook methods are optional extension points with default implementations (often empty). Subclasses can override them but aren’t required to. Use them for optional behavior or when a reasonable default exists.

class ReportGenerator(ABC):
    def generate(self, data):
        self._authenticate()        # Hook - optional security
        validated = self._validate(data)
        processed = self._process(validated)  # Abstract - required
        formatted = self._format(processed)   # Abstract - required
        self._before_output()       # Hook - optional setup
        self._output(formatted)
        self._cleanup()             # Hook - optional cleanup
    
    def _authenticate(self) -> None:
        """Hook: Override to add authentication."""
        pass
    
    @abstractmethod
    def _process(self, data) -> Any:
        """Required: Subclass must implement data processing."""
        pass
    
    @abstractmethod
    def _format(self, data) -> str:
        """Required: Subclass must implement formatting."""
        pass
    
    def _before_output(self) -> None:
        """Hook: Override to add pre-output behavior."""
        pass
    
    def _output(self, content: str) -> None:
        """Concrete: Default output behavior."""
        print(content)
    
    def _cleanup(self) -> None:
        """Hook: Override to add cleanup logic."""
        pass


class SecureReportGenerator(ReportGenerator):
    """Subclass that uses authentication hook."""
    
    def _authenticate(self) -> None:
        # Override hook to add security
        if not self._check_permissions():
            raise PermissionError("Access denied")
    
    def _check_permissions(self) -> bool:
        # Actual permission check logic
        return True
    
    def _process(self, data):
        return [item for item in data if item.get("visible", True)]
    
    def _format(self, data) -> str:
        return "\n".join(str(item) for item in data)

Template Method vs. Strategy Pattern

These patterns solve similar problems but with different trade-offs.

Template Method uses inheritance. The algorithm structure is fixed at compile time. Subclasses customize steps but can’t change the overall flow.

Strategy uses composition. Algorithms are interchangeable at runtime. The context delegates to strategy objects rather than inheriting behavior.

# Template Method approach
class Sorter(ABC):
    def sort(self, items):
        self._prepare(items)
        return self._do_sort(items)
    
    def _prepare(self, items):
        print(f"Sorting {len(items)} items")
    
    @abstractmethod
    def _do_sort(self, items):
        pass

class QuickSorter(Sorter):
    def _do_sort(self, items):
        # Quick sort implementation
        if len(items) <= 1:
            return items
        pivot = items[len(items) // 2]
        left = [x for x in items if x < pivot]
        middle = [x for x in items if x == pivot]
        right = [x for x in items if x > pivot]
        return self._do_sort(left) + middle + self._do_sort(right)


# Strategy approach
class SortStrategy(ABC):
    @abstractmethod
    def sort(self, items):
        pass

class QuickSortStrategy(SortStrategy):
    def sort(self, items):
        if len(items) <= 1:
            return items
        pivot = items[len(items) // 2]
        left = [x for x in items if x < pivot]
        middle = [x for x in items if x == pivot]
        right = [x for x in items if x > pivot]
        return self.sort(left) + middle + self.sort(right)

class SortContext:
    def __init__(self, strategy: SortStrategy):
        self._strategy = strategy
    
    def set_strategy(self, strategy: SortStrategy):
        self._strategy = strategy  # Can change at runtime
    
    def sort(self, items):
        print(f"Sorting {len(items)} items")
        return self._strategy.sort(items)

Choose Template Method when the algorithm structure is invariant and you only need to customize specific steps. Choose Strategy when you need runtime flexibility or want to avoid deep inheritance hierarchies.

Best Practices and Pitfalls

Keep template methods non-overridable. In languages that support it, mark template methods as final to prevent subclasses from breaking the algorithm structure.

Minimize abstract methods. Too many required extension points make the pattern unwieldy and force subclasses to implement methods they don’t care about.

# Anti-pattern: Too many extension points
class OverengineeredProcessor(ABC):
    def process(self):
        self._step1()
        self._step2()
        self._step3()
        self._step4()
        self._step5()
        self._step6()
        self._step7()  # Subclasses must implement 7 methods!
    
    @abstractmethod
    def _step1(self): pass
    @abstractmethod
    def _step2(self): pass
    # ... this is painful to extend

Watch for the fragile base class problem. Changes to the base class can break subclasses in unexpected ways. Document which methods are safe to override and which call sequences subclasses can rely on.

Test at both levels. Test the template method in the base class to verify the algorithm structure. Test concrete classes to verify their specific implementations.

The Template Method pattern shines when you have a stable algorithm with predictable variation points. Use it to eliminate duplication while maintaining flexibility, but don’t reach for it when simpler solutions—or the Strategy pattern—would serve better.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.