Template Method (fixed skeleton, overridable steps)
When to use
- You have a standard workflow (extract → transform → load) that should be the same everywhere.
- Each job varies in how a step is done, not which steps exist.
- You want code reuse: one
run()defines the flow; subclasses fill in details.
Avoid when steps themselves vary (different order/extra steps) → prefer Strategy/Composite.
Diagram (text)
ETLTemplate.run()
├─ extract() (must override)
├─ transform() (optional hook, default = identity)
└─ load() (must override)
DailyEventsJob ── overrides extract/transform/load
Python example (≤40 lines, type-hinted)
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Iterable, Dict
Row = Dict[str, object]
@dataclass
class ETLTemplate(ABC):
def run(self) -> int:
rows = self.extract()
rows = self.transform(rows) # optional hook
return self.load(rows)
@abstractmethod
def extract(self) -> Iterable[Row]: ...
def transform(self, rows: Iterable[Row]) -> Iterable[Row]: # default no-op
return rows
@abstractmethod
def load(self, rows: Iterable[Row]) -> int: ...
@dataclass
class DailyEventsJob(ETLTemplate):
source: list[Row]
sink: list[Row]
def extract(self) -> Iterable[Row]:
return self.source
def transform(self, rows: Iterable[Row]) -> Iterable[Row]:
return (r for r in rows if r.get("type") == "event")
def load(self, rows: Iterable[Row]) -> int:
n = 0
for r in rows: self.sink.append(r); n += 1
return n
Tiny pytest (cements it)
def test_template_method_runs_in_order():
src, out = [{"type":"event"}, {"type":"other"}], []
job = DailyEventsJob(src, out)
assert job.run() == 1
assert out == [{"type":"event"}]
Trade-offs & pitfalls
- Pros: One place defines the workflow; consistent behavior; subclasses are small; easy testing of the skeleton.
- Cons: Rigid step order; many subclasses if variations explode.
- Pitfalls:
- Stuffing business logic into the base
run()—keep it generic. - Too many hooks (“kitchen-sink” base).
- Hidden state leaking between runs—prefer pure steps or explicit state.
- Stuffing business logic into the base
Pythonic alternatives
- Composition + Strategy: inject
extract/transform/loadcallables into one job class. - Generator pipeline:
rows = (transform(r) for r in extract()); load(rows). - Context managers for per-run setup/teardown around
run()if needed.
Mini exercise
Add optional hooks before() and after(count: int) in ETLTemplate (default no-ops). Override them in DailyEventsJob to record metrics in sink as {"metrics": count}. Test that they’re called and count is correct.
Checks (quick checklist)
run()defines the fixed skeleton once.- Only minimal abstract methods; optional hooks have safe defaults.
- Subclasses override how, not what steps exist.
- Tests verify order and result, not just outputs.
- Prefer Strategy/Composite if steps/ordering must vary.




