Every night, PideYa generates each restaurant's daily closing report: the day's orders are loaded, the figures aggregated, the result formatted and distributed. The flow is always the same; what changes are specific steps: the PDF report for the owner formats and distributes differently than the CSV for accounting or the summary sent by email. Strategy taught us to swap complete algorithms; here the complete algorithm is shared and only pieces vary. For this, GoF reserves its inheritance pattern par excellence: the algorithm's skeleton lives in a base class, fixed and untouchable, and the subclasses fill in the gaps — under the Hollywood principle: "don't call us; we'll call you".

Contents

  1. The problem in PideYa: reports that copy-paste the flow
  2. Intent and structure of the pattern
  3. Complete Java implementation
  4. Hooks: the optional steps
  5. The Hollywood principle
  6. You've already seen this pattern (we just didn't name it)
  7. Template Method vs. Strategy: inheritance versus composition
  8. When to use it and when not to
  9. Relationship with other patterns
  10. Common mistakes
  11. Exercises and conclusion

The problem in PideYa: reports that copy-paste the flow

Today there are PdfClosingReport and CsvClosingReport, and they're twins by copy-and-paste:

public class PdfClosingReport {
    public void generate(String restaurantId, LocalDate day) {
        List<Order> orders = repository.ordersOfDay(restaurantId, day);         // same
        orders = orders.stream().filter(o -> o.getStatus() == DELIVERED).toList(); // same
        ClosingFigures figures = aggregate(orders);                             // same
        byte[] pdf = layoutPdf(figures);                  // ← different
        storage.save(restaurantId, day, pdf);             // ← different
    }
    // aggregate(...) duplicated here...
}

public class CsvClosingReport {
    public void generate(String restaurantId, LocalDate day) {
        // ...the same first four lines, copied...
        String csv = dumpCsv(figures);                    // ← different
        sftp.upload("accounting/" + day + ".csv", csv);   // ← different
    }
    // ...and aggregate(...) duplicated too
}

The exact symptom: two algorithms identical in structure that differ only in specific steps. You already know the consequences of copy-and-paste: when operations asks to "also exclude test orders", you have to remember to touch every copy of the filtering (and the third copy, the email report on its way, is born already out of sync). The shared flow has no owner: it lives duplicated in every variant.

The inversion that fixes it: the shared flow moves up into a base class as a single, untouchable method, and the variable steps move down into the subclasses as abstract methods. The base calls the subclasses — not the other way around.

Intent and structure of the pattern

Intent (GoF): define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.

This is one of the two class-scoped behavioral patterns (announced in the module introduction): the relationship is fixed through inheritance, at compile time.

classDiagram
    class ClosingReport {
        <<abstract>>
        +generate(restaurantId, day) «final»
        #loadOrders(restaurantId, day) List~Order~
        #aggregate(orders) ClosingFigures
        #format(figures)* byte[]
        #distribute(restaurantId, day, content)*
        #shouldGenerate(figures) boolean «hook»
    }
    class PdfClosingReport {
        #format(figures) byte[]
        #distribute(restaurantId, day, content)
    }
    class CsvClosingReport {
        #format(figures) byte[]
        #distribute(restaurantId, day, content)
    }
    class EmailSummaryReport {
        #format(figures) byte[]
        #distribute(restaurantId, day, content)
        #shouldGenerate(figures) boolean
    }

    ClosingReport <|-- PdfClosingReport
    ClosingReport <|-- CsvClosingReport
    ClosingReport <|-- EmailSummaryReport
GoF role In PideYa
AbstractClass (the template method + primitive steps) ClosingReport
ConcreteClass (implements the variable steps) PdfClosingReport, CsvClosingReport, EmailSummaryReport

The base class's methods fall into four kinds, and telling them apart is half the lesson:

Kind of method Example Do subclasses touch it?
Template method (the skeleton) generate(...) Never — declare it final
Concrete step (shared, implemented in the base) loadOrders, aggregate Not normally
Abstract step (mandatory in each subclass) format, distribute They must implement it
Hook (optional, with a default implementation) shouldGenerate They may, if they wish

Complete Java implementation

public abstract class ClosingReport {

    protected final OrderRepository repository;

    protected ClosingReport(OrderRepository repository) {
        this.repository = repository;
    }

    /** THE TEMPLATE METHOD: the complete algorithm, in one place, final. */
    public final void generate(String restaurantId, LocalDate day) {
        List<Order> orders = loadOrders(restaurantId, day);
        ClosingFigures figures = aggregate(orders);
        if (!shouldGenerate(figures)) {                  // hook: optional step
            EventLog.INSTANCE.info("Closing skipped for " + restaurantId);
            return;
        }
        byte[] content = format(figures);                // abstract step
        distribute(restaurantId, day, content);          // abstract step
    }

    /** Concrete step: shared by every report. A single copy of the filtering. */
    protected List<Order> loadOrders(String restaurantId, LocalDate day) {
        return repository.ordersOfDay(restaurantId, day).stream()
                .filter(o -> o.getStatus() == OrderState.DELIVERED)
                .toList();
    }

    /** Concrete step: the aggregation, previously duplicated, now with a single owner. */
    protected ClosingFigures aggregate(List<Order> orders) {
        BigDecimal total = orders.stream()
                .map(Order::getTotal)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        Map<String, Long> byDish = orders.stream()
                .flatMap(o -> o.getLines().stream())
                .collect(groupingBy(OrderLine::getDescription, counting()));
        return new ClosingFigures(orders.size(), total, byDish);
    }

    /** Hook: by default, every report gets generated. Subclasses may weigh in. */
    protected boolean shouldGenerate(ClosingFigures figures) {
        return true;
    }

    /** Abstract steps: every report MUST say how it formats and distributes. */
    protected abstract byte[] format(ClosingFigures figures);

    protected abstract void distribute(String restaurantId, LocalDate day, byte[] content);
}

The subclasses shrink down to their essential difference:

public class CsvClosingReport extends ClosingReport {

    private final SftpClient sftp;

    public CsvClosingReport(OrderRepository repo, SftpClient sftp) {
        super(repo);
        this.sftp = sftp;
    }

    @Override
    protected byte[] format(ClosingFigures figures) {
        StringBuilder csv = new StringBuilder("dish;units\n");
        figures.byDish().forEach((dish, n) -> csv.append(dish).append(';').append(n).append('\n'));
        return csv.toString().getBytes(StandardCharsets.UTF_8);
    }

    @Override
    protected void distribute(String restaurantId, LocalDate day, byte[] content) {
        sftp.upload("accounting/" + restaurantId + "/" + day + ".csv", content);
    }
}

Craft details in the base, all deliberate:

  • generate is final: the skeleton is the contract; if a subclass could redefine it, the pattern evaporates (and the copy-and-paste returns, now with inheritance).
  • The steps are protected: they are the interface toward the subclasses, not toward the world. Nobody outside calls format.
  • The filtering fix is now a single change: "exclude test orders" is touched in loadOrders and all three variants inherit it instantly.

Hooks: the optional steps

The shouldGenerate hook deserves its own section because it is the pattern's fine-grained flexibility tool: a method with a default implementation (often empty or trivial) that subclasses may override to hook into the flow without being forced to. The email summary report uses it: if the restaurant didn't open, there's nothing to send:

public class EmailSummaryReport extends ClosingReport {
    // ...
    @Override
    protected boolean shouldGenerate(ClosingFigures figures) {
        return figures.orderCount() > 0;        // no orders, no email
    }
}

Practical rules for designing hooks: few, and with names that betray the moment (shouldGenerate, onDistributionFinished); harmless by default (return the permissive value, do nothing); and documented in the base — they are the extension contract. Frameworks use them everywhere: Android's onCreate/onPause or Spring's lifecycle callbacks are hooks of giant templates.

The Hollywood principle

"Don't call us, we'll call you." The inversion of control that defines the pattern: the low-level code (the subclasses) does not call the flow; it's the flow (the base) that calls the subclasses when their turn comes. Compare it with a classic library: you call Math.max(...) whenever you want; in Template Method, CsvClosingReport never decides when to format — it gets called by generate at exactly the right moment.

This inversion is the seed of something you already use: frameworks work this way. Spring, JUnit, or Android define the flow (the template method, at industrial scale) and your code fills in gaps that the framework invokes. The difference between using a library and using a framework is the Hollywood principle — and you learned it here, in a pattern of a class and a half.

You've already seen this pattern (we just didn't name it)

Two reunions from the course itself:

  • The handle(...) of Chain of Responsibility: a public method that fixes the protocol (check → cut or delegate) by calling each link's abstract check. We called it the "template trick" back then; it was exactly this pattern in miniature.
  • The shared flow of the Bridge's Notification admits the same move: a final send() in the abstraction that does validate → compose → send through the channel, with an abstract compose() per notification type. Template Method inside the left side of a Bridge: patterns nest.

And in the JDK: AbstractList gives you add, contains, or indexOf for free if you implement get and size (templates over primitives), and JUnit tests with @BeforeEach/@Test/@AfterEach are a template method you never see that calls your methods.

Template Method vs. Strategy: inheritance versus composition

The head-to-head promised in the previous lesson — same problem (varying the "how" without touching the clients), solutions from the two schools:

Criterion Template Method Strategy
Mechanism Inheritance (class scope) Composition (object scope)
What varies Steps of an algorithm whose skeleton is fixed The entire algorithm
When the variant is decided At compile time/subclass instantiation At runtime; hot-swappable
Combining variants along several axes? Poorly: one subclass per combination Well: inject one strategy per axis
Access to shared state Direct (the base's protected fields) Only what the interface passes as parameters
Characteristic risk Rigid hierarchies, base-subclass coupling More pieces and injection wiring
In PideYa ClosingReport DeliveryFeeCalculation, AssignmentStrategy

The decision guide, honestly: if the variable steps need to share a lot of the flow's context and the variants are few and stable, Template Method is direct and concise. If you need to change the variant at runtime, combine axes of variation, or test the variants in isolation from the flow, Strategy — in fact, the typical evolution is starting with Template Method and, when the subclasses multiply combinatorially, extracting each variable step as an injected strategy ("composition over inheritance" has its history running since module 1, and this is its classic battleground). Both coexist well: a template method whose steps delegate to strategies.

When to use it and when not to

Use it when:

  • Several classes implement the same algorithm with different steps and the copy-and-paste already hurts (or is about to: a third variant in sight).
  • You want to fix the ordering of the flow as an inviolable contract and offer controlled extension points (steps and hooks).
  • You're building a reusable base or internal mini-framework where other teams fill in the gaps.

Avoid it when:

  • The variants need to change at runtime or be combined: inheritance fixes everything at construction — Strategy.
  • The "common part" is small or forced: an artificial base to share two lines couples entire hierarchies for nothing; sometimes a shared static method is the boring, correct answer.
  • A hierarchy already exists for another reason: inheritance is a one-shot resource in Java; spending it on the report flow prevents using it for anything else.

Relationship with other patterns

  • Strategy: the table above; template steps extractable as strategies.
  • Factory Method: GoF literally defines it as "a specialization of Template Method" where the variable step is creating an object — our old acquaintance from module 2, reframed.
  • Chain of Responsibility and Bridge: the reunions from the previous section.
  • Builder: a builder's director executes construction steps in a fixed order — template spirit in the creational world.

Common mistakes

  • A non-final template method: a subclass redefines it "just for this case" and the single skeleton fragments. final isn't paranoia; it's the pattern.
  • Too many abstract steps: if the base delegates everything, there's no template — just an expensive interface. The base must own the flow and most of the shared work.
  • Hooks that subclasses are obliged to call (super.onFinished() "or everything breaks"): inheritance's classic fragile contract. Design the base so the mandatory parts live in the template, not in the good memory of whoever inherits.
  • Subclasses that bypass the flow by calling the steps directly (format from outside generate): protected visibility and discipline; the steps are not public API.
  • Generously shared protected mutable state: the subclasses end up depending on the base's guts and every base refactor breaks children. Pass data as step parameters (like our ClosingFigures) whenever you can.

Exercises

Exercise 1: a new variant in ten lines

Implement JsonClosingReport: format the figures as JSON (use whatever format you like) and publish them to datalakeStore.publish("closings/" + day + "/" + restaurantId + ".json", content). How many lines of flow (load/filter/aggregate) did you write?

Exercise 2: the after-hook

Operations wants each report, after distributing successfully, to optionally record metrics (the PDF doesn't; the CSV must record metrics.recordAccountingUpload(day)). Add the hook to the base (name, signature, default implementation, call site in generate) and use it in CsvClosingReport.

Exercise 3: template or strategy?

The notifications team wants to unify the flow validate recipient → compose message → send → log: (a) if the variants are "confirmation", "delay", and "promotion", differing only in compose, what would you use? (b) and what if the sending must also be switchable at runtime between push/SMS/email based on user preferences? Relate your answer to what was built in module 3's Bridge.

Solutions

Solution 1:

public class JsonClosingReport extends ClosingReport {

    private final DatalakeStore datalakeStore;

    public JsonClosingReport(OrderRepository repo, DatalakeStore store) {
        super(repo);
        this.datalakeStore = store;
    }

    @Override
    protected byte[] format(ClosingFigures figures) {
        String json = "{\"orders\":" + figures.orderCount()
                    + ",\"total\":" + figures.total() + "}";
        return json.getBytes(StandardCharsets.UTF_8);
    }

    @Override
    protected void distribute(String restaurantId, LocalDate day, byte[] content) {
        datalakeStore.publish("closings/" + day + "/" + restaurantId + ".json", content);
    }
}

Lines of flow written: zero — loading, filtering, and aggregating come inherited, future fixes included. That is the pattern's dividend.

Solution 2: in the base:

/** Hook: called after a completed distribution. By default, nothing. */
protected void onDistributed(String restaurantId, LocalDate day, ClosingFigures figures) {
}

and in the template method, the last line of generate becomes:

distribute(restaurantId, day, content);
onDistributed(restaurantId, day, figures);   // the flow calls the hook (Hollywood)

In CsvClosingReport:

@Override
protected void onDistributed(String restaurantId, LocalDate day, ClosingFigures figures) {
    metrics.recordAccountingUpload(day);
}

The PDF touches nothing: hooks don't oblige. (And note the ordering: the hook goes after distribute in the template, not inside each distribute — the moment of the call is the skeleton's property.)

Solution 3: (a) Template Method: a fixed skeleton, a single variable step (compose), stable variants — a template with an abstract compose(), exactly the move sketched in the reunions section; (b) the channel axis must change at runtime and per user: inheritance cannot fix that — and it's precisely what the Bridge already solved: the Notification abstraction (where the template method with per-type compose can live via inheritance) delegates the sending to a composed, swappable DeliveryChannel. In other words: the correct answer is both, each on its own axis — a template for the flow and its per-type steps, composition for the channel. Independent axes of variation call for composition; the steps of a fixed flow tolerate inheritance.

Conclusion

Template Method puts the shared flow under a single owner: ClosingReport.generate(...) fixes the skeleton — load, aggregate, generate?, format, distribute — as a final method, the shared logic lives exactly once, and each variant (PDF, CSV, email, and the exercise's JSON) contributes only its difference, invoked by the base when its turn comes: Hollywood in its purest form, the same inversion of control through which the frameworks you use daily talk to you. And Strategy's third border got drawn: steps of a fixed flow → inheritance and a template; complete algorithms, combinable or hot-swappable → composition and strategies.

One last pattern remains in the behavioral catalog, and it attacks the most twisted problem of all: adding new operations to a stable hierarchy without touching it. The Composite menu has spent two modules collecting suitors — export to JSON, compute allergens, audit prices — and stuffing each operation into Dish and MenuSection would turn them into junk drawers. The solution demands GoF's most elegant technical trick: double dispatch. See you in Visitor.

© Copyright 2026. All rights reserved