Template Method Pattern: Fixing the Skeleton, Letting Subclasses Fill In Steps
Define the invariant shape of an algorithm once in a base class, and let subclasses override only the steps that actually vary — the Hollywood Principle in practice.
Template Method Pattern
Define the skeleton of an algorithm in a base class method, and defer specific steps to subclasses. The base class controls the order and invariant structure of the algorithm; subclasses control only the details of individual steps. This is inheritance used correctly: not "reuse some fields," but "reuse a fixed control flow while varying specific, well-named extension points."
1. The Problem: Duplicated Skeletons
Several report generators share 90% of the same flow — fetch data, validate, transform, render, save — but each duplicates that entire flow to change one or two steps. The skeleton itself gets copy-pasted, and a bug fix to "validate" now needs to be applied in three places.
// VIOLATION: three classes, each re-implementing an almost-identical pipeline
class CsvReportGenerator {
void generate(String source) {
System.out.println("Connecting to " + source);
String rawData = "raw-data"; // fetch
if (rawData == null) throw new IllegalStateException("no data"); // validate
String transformed = rawData.toUpperCase(); // transform (varies)
System.out.println("Writing CSV: " + transformed); // render (varies)
System.out.println("Saved report.csv"); // save
}
}
class PdfReportGenerator {
void generate(String source) {
System.out.println("Connecting to " + source);
String rawData = "raw-data"; // fetch — duplicated
if (rawData == null) throw new IllegalStateException("no data"); // validate — duplicated
String transformed = rawData.trim(); // transform (varies)
System.out.println("Rendering PDF: " + transformed); // render (varies)
System.out.println("Saved report.pdf"); // save — duplicated
}
}
// A fix to "validate" (e.g. also reject empty strings) must be applied to
// every generator individually — and it's easy to miss one.This is what naive code reuse-via-copy-paste looks like. The fix isn't "extract a helper method" (that still lets someone call steps out of order or skip one) — it's inverting control so the base class owns the order, and subclasses can't touch it.
2. Structure and the Hollywood Principle
Template Method embodies the Hollywood Principle: "Don't call us, we'll call you." Subclasses never call the base class's algorithm steps directly — the base class calls them, in the order it decides. Control flow lives exactly once, in the base class's final template method.
Three kinds of steps appear in a typical Template Method:
- Fixed steps — implemented once in the base class, never overridden (
fetchData,validate). - Abstract steps — declared
abstract, subclasses must provide an implementation (transform,render). - Hook methods — implemented in the base class with a no-op or sensible default, subclasses may override if they need to (
afterSave). Hooks are how Template Method stays flexible without forcing every subclass to implement every step.
3. Full Implementation
// The base class owns the algorithm's shape. generate() is final —
// no subclass can reorder steps or skip validation.
abstract class ReportGenerator {
// Template method: the fixed skeleton
public final void generate(String source) {
String rawData = fetchData(source);
validate(rawData);
String transformed = transform(rawData);
render(transformed);
save();
afterSave(); // hook — optional extension point
}
// Fixed step — identical for every subclass
protected String fetchData(String source) {
System.out.println("Connecting to " + source);
return "raw-data-from-" + source;
}
// Fixed step — identical for every subclass
protected void validate(String data) {
if (data == null || data.isBlank()) {
throw new IllegalStateException("No data to report on");
}
}
// Abstract step — every subclass MUST define its own transform
protected abstract String transform(String data);
// Abstract step — every subclass MUST define its own render
protected abstract void render(String data);
// Fixed step
private void save() {
System.out.println("Report saved.");
}
// Hook — default no-op; override only if a subclass needs post-save work
protected void afterSave() {
// no-op by default
}
}
class CsvReportGenerator extends ReportGenerator {
@Override
protected String transform(String data) {
return data.toUpperCase();
}
@Override
protected void render(String data) {
System.out.println("Writing CSV rows: " + data);
}
}
class PdfReportGenerator extends ReportGenerator {
@Override
protected String transform(String data) {
return data.trim();
}
@Override
protected void render(String data) {
System.out.println("Rendering PDF pages: " + data);
}
// Overrides the hook — PDF generation needs an extra step, CSV doesn't
@Override
protected void afterSave() {
System.out.println("Emailing PDF to distribution list...");
}
}
class Demo {
public static void main(String[] args) {
ReportGenerator csv = new CsvReportGenerator();
csv.generate("orders-db");
ReportGenerator pdf = new PdfReportGenerator();
pdf.generate("orders-db");
}
}validate() is fixed once. Fixing a bug there fixes it for every report type, forever — there is no second copy to forget.
4. Runtime Sequence
Note that Client only ever calls generate() — it never calls transform(), render(), or afterSave() directly. The base class is the only thing that invokes those steps, and always in the same fixed order. That is the Hollywood Principle rendered as a sequence diagram.
5. A Second Worked Example: Data Import Pipeline
To see the pattern generalize beyond reports, here is the same shape applied to importing records from different file formats into a database — a common real backend task.
abstract class DataImporter<T> {
// Template method
public final ImportResult importFile(Path filePath) {
List<String> lines = readLines(filePath);
List<T> records = parseRecords(lines);
List<T> validRecords = filterInvalid(records);
int saved = persist(validRecords);
onImportComplete(saved, records.size() - validRecords.size());
return new ImportResult(saved, records.size() - validRecords.size());
}
// Fixed step
private List<String> readLines(Path filePath) {
try {
return Files.readAllLines(filePath);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
// Abstract — format-specific parsing (CSV vs JSON vs fixed-width)
protected abstract List<T> parseRecords(List<String> lines);
// Fixed step — reused validation logic
private List<T> filterInvalid(List<T> records) {
return records.stream().filter(this::isValid).toList();
}
// Abstract — each record type validates differently
protected abstract boolean isValid(T record);
// Abstract — persistence target varies (SQL table, document store, etc.)
protected abstract int persist(List<T> validRecords);
// Hook — default no-op; only some importers need a completion notification
protected void onImportComplete(int saved, int rejected) {
// no-op by default
}
}
record CustomerRecord(String name, String email) {}
class CustomerCsvImporter extends DataImporter<CustomerRecord> {
@Override
protected List<CustomerRecord> parseRecords(List<String> lines) {
return lines.stream()
.skip(1) // header row
.map(line -> line.split(","))
.map(parts -> new CustomerRecord(parts[0], parts[1]))
.toList();
}
@Override
protected boolean isValid(CustomerRecord record) {
return record.email().contains("@");
}
@Override
protected int persist(List<CustomerRecord> validRecords) {
System.out.println("Saving " + validRecords.size() + " customers");
return validRecords.size();
}
@Override
protected void onImportComplete(int saved, int rejected) {
System.out.printf("Import complete: %d saved, %d rejected%n", saved, rejected);
}
}
record ImportResult(int saved, int rejected) {}CustomerJsonImporter or CustomerFixedWidthImporter would each override only parseRecords — readLines, filterInvalid, and the overall order stay defined exactly once, in DataImporter.
6. Testing a Template Method Hierarchy
Testing splits cleanly along the same lines as the pattern itself: test the fixed skeleton once (via any concrete subclass, or a minimal test-only subclass), and test each subclass's abstract-step overrides independently.
class CustomerCsvImporterTest {
@Test
void parsesValidRowsAndSkipsHeader() {
CustomerCsvImporter importer = new CustomerCsvImporter();
// Testing parseRecords() in isolation would require a package-private
// test seam, OR testing through the public template method:
Path tempFile = writeTempCsv("name,email\nArvind,arvind@example.com\nBad,not-an-email");
ImportResult result = importer.importFile(tempFile);
assertEquals(1, result.saved());
assertEquals(1, result.rejected());
}
private Path writeTempCsv(String content) { /* ... */ return null; }
}
// A minimal test-only subclass can validate the SKELETON itself,
// independent of any real subclass's business logic:
class SkeletonOrderTest {
static class RecordingImporter extends DataImporter<String> {
List<String> callOrder = new ArrayList<>();
protected List<String> parseRecords(List<String> lines) {
callOrder.add("parse"); return lines;
}
protected boolean isValid(String r) { callOrder.add("validate"); return true; }
protected int persist(List<String> valid) { callOrder.add("persist"); return valid.size(); }
}
@Test
void stepsRunInFixedOrder() {
RecordingImporter importer = new RecordingImporter();
importer.importFile(Path.of("dummy.csv"));
assertEquals(List.of("parse", "validate", "persist"), importer.callOrder);
}
}Because generate()/importFile() is final, you cannot mock or stub the skeleton itself in a unit test — which is the point. If you find yourself wanting to test the skeleton's order independent of any real subclass's logic, a minimal recording subclass (as above) is the standard technique.
7. Common Pitfalls
Pitfall 1 — The fragile base class problem. Because subclasses depend on the internal structure of the base class (which steps exist, in what order, with what visibility), a change to the base class can silently break every subclass. Changing validate()'s fixed logic, for instance, changes behavior for every report type simultaneously — usually desirable, but it means the base class carries more blast radius than an equivalent Strategy change would.
Pitfall 2 — Overriding every step. If a new subclass ends up overriding fetchData, validate, transform, render, and afterSave, there is no shared skeleton left — you've built an elaborate empty interface with inheritance ceremony. That's the signal to switch to Strategy instead.
Pitfall 3 — Forgetting to make the template method final. Without final, a subclass can override generate() itself, reorder the steps, or skip validate() entirely — silently defeating the entire point of the pattern. Always mark the template method final (or, in languages without final, document and enforce it via code review/lint rules).
Pitfall 4 — Deep hierarchies. ReportGenerator → StructuredReportGenerator → CsvReportGenerator → TabDelimitedCsvReportGenerator makes it increasingly hard to know which class actually defines a given step's behavior. Prefer one level of subclassing under the template; if you need more variation, compose a Strategy into a leaf subclass rather than adding another inheritance layer.
8. When to Use vs. When It's Overkill
| Use Template Method when | Skip it when |
|---|---|
| Multiple classes share an identical sequence of steps, varying only in a few of them | The "shared flow" is only two lines — an extracted helper method is simpler |
| You want to enforce step order (subclasses literally cannot reorder or skip a step) | Callers legitimately need to call steps independently or out of order |
| The variation is a small, closed set of override points known up front | The set of varying steps is itself unstable or changes with every new subclass |
| Inheritance is already the natural relationship (these are specializations of the same process) | The "subclasses" don't share an is-a relationship — composition (Strategy) fits better |
Over-applying Template Method: if subclasses end up overriding every step, the base class isn't providing a skeleton — it's providing an empty interface with extra ceremony. That's a sign you actually want Strategy (each variant is a fully independent algorithm) rather than a shared template with one meaningful abstract step.
9. Template Method vs. Strategy
The two patterns are frequently taught together because they solve the same category of problem — "vary an algorithm" — with opposite mechanisms:
| Template Method | Strategy | |
|---|---|---|
| Mechanism | Inheritance — override specific steps | Composition — inject a whole algorithm object |
| Granularity of variation | Fine-grained: individual steps of a fixed flow | Coarse-grained: the entire algorithm as one unit |
| Flexibility | Fixed at compile time (subclass is chosen once) | Swappable at runtime (inject a new strategy any time) |
| Control | Base class calls subclass (Hollywood Principle) | Context calls whichever strategy object it holds |
| Risk | Deep hierarchies, fragile base class problem | Extra interface + class per variant |
A concrete way to decide: if you're overriding one or two methods of an otherwise-identical process shared with siblings, that's Template Method. If you're building a full drop-in replacement for the whole algorithm — no shared base logic at all — that's Strategy. It is common to see both in the same codebase: a ReportGenerator template method whose render() step internally delegates to an injected RenderStrategy. See Strategy Pattern for the composition-first version of this problem.
10. Real-World / Production Examples
java.util.AbstractList / AbstractMap — the JDK collections framework is built on Template Method: you implement get(int) and size(), and AbstractList gives you iterator(), contains(), indexOf() for free, built on top of your two methods.
JUnit / TestNG lifecycle — @BeforeEach → test method → @AfterEach is a template; the framework owns the order, your test class fills in the steps. TestCase.runBare() in JUnit 3 was a literal, explicit Template Method.
Spring's JdbcTemplate — you provide a RowMapper (the varying step); JdbcTemplate owns the fixed skeleton of "open connection, execute, map rows, close connection, handle exceptions."
List<Customer> customers = jdbcTemplate.query(
"SELECT * FROM customers",
(rs, rowNum) -> new Customer(rs.getString("name"), rs.getString("email"))
// ^ this lambda is the ONE varying step; connection handling, exception
// translation, and resource cleanup are all fixed inside JdbcTemplate
);HttpServlet — service() is the template method; it dispatches to doGet(), doPost(), etc., which subclasses override. You never call service() yourself — the container does (Hollywood Principle, literally).
Build pipelines — a BuildPipeline base class fixing "checkout → compile → test → package → publish," where language-specific subclasses (MavenBuildPipeline, GradleBuildPipeline) override compile() and test().
11. Combining Template Method with Strategy
The two patterns are not mutually exclusive — a common, pragmatic design uses Template Method for the parts of the flow that are genuinely structural (order, fixed steps), and injects a Strategy for the one step that needs runtime, not just compile-time, flexibility:
abstract class ReportGenerator {
private final RenderStrategy renderStrategy; // composition for THIS one step
protected ReportGenerator(RenderStrategy renderStrategy) {
this.renderStrategy = renderStrategy;
}
public final void generate(String source) {
String rawData = fetchData(source);
validate(rawData);
String transformed = transform(rawData);
renderStrategy.render(transformed); // delegate to an injected strategy, not a subclass override
save();
}
protected String fetchData(String source) { /* fixed */ return "data"; }
protected void validate(String data) { /* fixed */ }
protected abstract String transform(String data); // still varies by inheritance
private void save() { /* fixed */ }
}
interface RenderStrategy {
void render(String data);
}Here, transform() varies by subclass (compile-time — a CSV importer will always transform the same way), while render() varies by injected strategy (runtime — the same importer could render to console today and to a file tomorrow, without a new subclass). Recognizing which axis of variation belongs to which pattern is exactly the skill this comparison is testing.
Interview Questions
- What does "Don't call us, we'll call you" (the Hollywood Principle) mean concretely in Template Method? Which class calls which?
- Why should the template method itself be declared
final? What breaks if a subclass can override it? - What's the difference between an abstract step and a hook method? Give an example of each.
- How does
AbstractListin the JDK use Template Method? What do you have to implement, and what do you get for free? - Contrast Template Method with Strategy: same problem (varying an algorithm), different mechanism — explain both, and when each is preferable.
- What is the "fragile base class problem," and how does it specifically threaten a deep Template Method hierarchy?
- If you find every subclass overriding every step of the template, what does that tell you about your design choice?
- How would you refactor three classes with 90% duplicated logic and one differing step into a Template Method hierarchy? Walk through the steps.
- How would you unit test the fixed skeleton's step order independent of any single subclass's business logic?
Quick Reference
| Question | Answer |
|---|---|
| What does Template Method fix? | The algorithm's skeleton and step order |
| What's the extension point? | Overriding abstract steps or hooks in a subclass |
| When is the variant chosen? | Compile time — by which subclass you instantiate |
| What GoF category? | Behavioral |
| Closest sibling pattern | Strategy (composition, not inheritance) |
| Typical JDK example | AbstractList, HttpServlet.service() |