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.
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 befinalwhen 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
| Variant | Description | Use Case |
|---|---|---|
| Classic | Inheritance-based with abstract methods | Frameworks, parsers |
| Strategy-based | Composition with strategy objects injected | When you need runtime flexibility |
| Callback-based | Functions passed as arguments | JavaScript/Node.js streams |
What Works
- Make the template method
finalto 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. Related Resources
Strategy Pattern
Define a family of algorithms, encapsulate each one, and make them interchangeable. A behavioral design pattern for flexible behavior selection.
PatternDecorator Pattern
Add new functionality to objects dynamically by wrapping them. A structural design pattern for flexible behavior extension.
PatternFactory Pattern
Create objects without specifying the exact class to instantiate. A creational design pattern for flexible object creation.
PatternCommand Pattern
Encapsulate a request as an object, letting you parameterize clients with queues, logs, and undoable operations. A behavioral design pattern.
PatternInterpreter Pattern
Define a representation for a language's grammar along with an interpreter that uses the representation to interpret sentences. A behavioral design pattern for mini-languages.
PatternIterator Pattern
Provide a way to access elements of a collection sequentially without exposing its underlying representation. A behavioral design pattern for traversal.