04-behavioral-patterns

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.

August 11, 2026
lldbehavioralstrategytemplate-methodalgorithminterchangeablehooks

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.

java
// 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

java
// 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.

java
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 parseRecordsreadLines, 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.

java
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. ReportGeneratorStructuredReportGeneratorCsvReportGeneratorTabDelimitedCsvReportGenerator 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 whenSkip it when
Multiple classes share an identical sequence of steps, varying only in a few of themThe "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 frontThe 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 MethodStrategy
MechanismInheritance — override specific stepsComposition — inject a whole algorithm object
Granularity of variationFine-grained: individual steps of a fixed flowCoarse-grained: the entire algorithm as one unit
FlexibilityFixed at compile time (subclass is chosen once)Swappable at runtime (inject a new strategy any time)
ControlBase class calls subclass (Hollywood Principle)Context calls whichever strategy object it holds
RiskDeep hierarchies, fragile base class problemExtra 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."

java
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
);

HttpServletservice() 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:

java
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 AbstractList in 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

QuestionAnswer
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 patternStrategy (composition, not inheritance)
Typical JDK exampleAbstractList, HttpServlet.service()