Skip to content

9.4.17 — Template Method

What the original Gang of Four book says: Define the skeleton of an algorithm in an operation, leaving some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.

What that means when you are actually writing code: You have five functions that do the same six things in the same order, and only two of those steps differ. Write the order down once in a parent class, and let each child fill in the two steps that are actually different.

Template Method is the oldest and most controversial pattern in this catalogue. It is old because it is simply what inheritance was invented for. It is controversial because it is the pattern most likely to be the wrong answer in modern code, where composition is usually better.

Both of those things are true at once, and this chapter takes both seriously. You need to understand Template Method because you will meet it constantly in frameworks and in older codebases, and because there is a narrow set of situations where it is genuinely the cleanest tool available. You also need to understand exactly when to reach for Strategy instead, because that is the more common correct answer.

1. The story: five importers that were ninety percent identical

A reporting system imports data from several sources. Each importer was written by a different person, at a different time, and each one looks broadly like this:

typescript
async function importCsvOrders(file: Buffer): Promise<ImportResult> {     
  const started = Date.now();
  log.info("csv import starting");
  const rows = parseCsv(file);                          // ← the only genuinely different line
  const valid: Order[] = [];
  const errors: RowError[] = [];
  for (const [i, row] of rows.entries()) {
    const result = orderSchema.safeParse(row);
    if (result.success) valid.push(result.data);
    else errors.push({ line: i + 1, issues: result.error.issues });
  }
  if (errors.length > rows.length * 0.1) throw new TooManyErrors(errors);
  await db.transaction(async (tx) => { for (const o of valid) await tx.orders.upsert(o); });
  await cache.invalidate("orders");
  log.info({ ms: Date.now() - started, ok: valid.length, failed: errors.length }, "csv import done");
  return { imported: valid.length, errors };
}

Next to it in the same folder sit importXmlOrders, importJsonOrders, importExcelOrders and importApiOrders. Every one of them is a near-perfect copy of the function above. The parsing line differs. In two of them the error threshold differs. Everything else is identical, character for character.

Here is what that costs, and the list is the argument for the pattern.

A change to the shared sequence has to be made five times. Somebody adds a step to record import metrics. They update three importers, miss two, and now two data sources silently have no metrics. Nobody notices for months, because missing metrics do not throw exceptions.

The bug fixes drift apart. Somebody discovers that the transaction should be committed before the cache is cleared rather than after, because clearing first creates a window where a reader repopulates the cache with stale data. They fix it in the CSV importer, where the bug was reported. The other four keep the bug, and it will be rediscovered separately in each of them over the following two years.

Nobody can see the shared algorithm. The sequence — parse, validate, check the error threshold, write in one transaction, clear the cache, log — is a real business rule about how imports work. It exists in five places and is written down in none of them. A new engineer has to read all five copies and mentally diff them to work out which parts are essential and which are accidental.

Adding a sixth importer means copying a fifth one. Whichever file you copy carries whatever bugs and whatever drift that particular file has accumulated. The copies get worse over time rather than better.

You cannot enforce anything. There is no way to guarantee that a new importer wraps its writes in a transaction, or checks the error threshold, or clears the cache. Every one of those is a convention that depends on the next engineer noticing.

Template Method fixes this by writing the sequence down exactly once, in a place the children cannot change, while leaving holes for the parts that genuinely differ:

typescript
class CsvOrderImporter extends OrderImporter {
  protected parse(file: Buffer): unknown[] { return parseCsv(file); }   // ← fill in the one hole
}

2. How you arrive at the pattern

Step 1 — Start naive. Write each variant as its own complete function. That is right when the variants are genuinely different, or when there are only two of them and they share little.

Step 2 — Wait for the force. Several procedures follow the same steps in the same order, and only a small number of those steps differ. Critically, the order itself is meaningful and must not vary, because if the order could vary then you are looking at something else entirely.

Step 3 — Draw the line between what varies and what stays fixed. Notice that this line runs the opposite way round compared with Strategy:

What variesa few individual steps inside a fixed procedure
What stays fixedthe order of the steps, and the rules applied around them

In Strategy, the whole algorithm varies and the caller picks one. In Template Method, the algorithm's shape is the thing you are protecting, and only named holes inside it can be filled.

Step 4 — Decide when the choice gets made. At compile time, by writing a subclass. This is the earliest binding time of any pattern in this catalogue, and it is the single biggest difference from Strategy.

Because the choice is made by inheritance, a class can only ever be one variant, and that variant can never be changed at runtime, swapped for testing, or combined with another.

Step 5 — Name the pattern and be honest about the costs. The name is Template Method, and the costs are genuinely heavier than most patterns here.

The first cost is inheritance, with everything that comes with it. A subclass is tightly bound to its parent's internals, which is the tightest coupling available in object-oriented programming. Change a protected method's signature and every subclass breaks.

The second cost is the fragile base class problem. When the parent changes, every subclass may be affected in ways that are hard to see, because the parent is calling into methods the subclass overrode. This is worse than ordinary coupling because the call direction is inverted.

The third cost is that one class can only be one variant. You cannot mix two, and you cannot change your mind at runtime.

The fourth cost is that reading the code requires jumping between files. To understand what CsvOrderImporter actually does, you have to read the parent and mentally weave the child's overrides into it. With three levels of inheritance, this becomes genuinely difficult.

The fifth cost is that it invites deep hierarchies. Somebody adds BaseImporter, then FileImporter extends BaseImporter, then DelimitedFileImporter extends FileImporter, and by then nobody can say what any concrete class does without reading four files.

What you get in return is that the shared sequence is written once and cannot be bypassed, that a new variant only has to supply what is genuinely different, and — the benefit that composition cannot easily match — that the parent can enforce rules on its children.

3. The mental model

In one sentence: Template Method is a fill-in-the-blanks form, where the parent writes the sentence and the child writes only the words in the gaps.

The analogy that makes it stick — a recipe for a cake. Every cake follows the same procedure: preheat the oven, mix the dry ingredients, mix the wet ingredients, combine them, bake, and cool. What varies is which dry ingredients and which flavouring go in. It does not vary that you mix before you bake, and a "recipe" that baked before mixing would not be a cake recipe at all.

That last point is the heart of the pattern. The order is not a convenience; it is the thing being protected.

A second analogy for the enforcement idea — a tax form. The form decides which boxes exist, what order they appear in, and which arithmetic gets applied to them. You fill in the boxes. You do not get to add a box, remove one, or change the arithmetic. The form is the template method, and your entries are the overridden steps.

When to reach for it. The signals are:

  • several classes or functions repeat the same sequence of steps with small differences
  • the phrase "the order must always be this" describes a real requirement
  • you are writing a framework or library and want users to plug into fixed extension points
  • there is setup and cleanup that must always happen, such as opening and closing a connection, or starting and committing a transaction
  • you want to guarantee that subclasses cannot skip a required step

When not to reach for it. If the variants need to be swapped at runtime, or combined with each other, or tested in isolation without building a subclass, use Strategy instead. If more than about half the steps are overridden, the "shared skeleton" is not really shared, and the inheritance is buying you nothing while costing you plenty.

4. Structure

OrderImporter (parent)import() — final, cannot be overridden① log start, begin timer② parse() — HOLE, child fills③ validate rows, collect errors④ errorThreshold() — HOLE, has default⑤ write in transaction, clear cache, logCsvOrderImporterparse() → parseCsv(file)XmlOrderImporterparse() → parseXml(file)ApiOrderImporterparse() → fetchAndParse()errorThreshold() → 0.25The grey steps are written once and no subclass can change or skip them. Only the green holes can be filled.
Figure 17 — A fixed skeleton with named holes. The parent (purple zone) owns the order of the steps and the rules around them. Grey steps are written once and cannot be overridden. Green steps are the extension points. Two subclasses (blue) fill only the parsing hole, while the third (amber) also overrides the error threshold, because its data source is known to be messier. Compare this with Strategy, where the whole algorithm is replaced rather than a step being filled in.

The participants are simple. The abstract class contains the template method, which is the fixed sequence, plus the abstract or default steps. Each concrete subclass implements the steps it must and overrides the ones it wants to.

There are three kinds of step inside a template method, and knowing the difference is what makes a good implementation:

Kind of stepDeclared asMeaning
Fixed stepprivate in the parentalways runs, never overridable
Required hookabstract protectedevery subclass must supply it
Optional hookprotected with a defaultsubclasses may override; sensible default provided

Getting these three right is most of the design. A required hook that should have had a default forces every subclass to write boilerplate. An optional hook that should have been required lets a subclass silently do nothing.

5. The code, walked through line by line

typescript
export abstract class OrderImporter {
  public async import(file: Buffer): Promise<ImportResult> {   // (1) the template method — FINAL
    const started = Date.now();
    this.log.info({ importer: this.name }, "import starting");

    const rows = this.parse(file);                             // (2) required hook
    const { valid, errors } = this.validate(rows);             // (3) fixed step

    if (errors.length > rows.length * this.errorThreshold()) { // (4) optional hook with a default
      throw new TooManyErrors(errors);
    }

    await this.db.transaction(async (tx) => {                  // (5) fixed: the rule being enforced
      for (const order of valid) await tx.orders.upsert(order);
      await this.afterWrite(tx, valid);                        // (6) optional hook, INSIDE the tx
    });

    await this.cache.invalidate("orders");                     // (7) fixed, and deliberately after
    this.log.info({ ms: Date.now() - started, ok: valid.length }, "import done");
    return { imported: valid.length, errors };
  }

  protected abstract get name(): string;                       // (8) required
  protected abstract parse(file: Buffer): unknown[];           //     required

  protected errorThreshold(): number { return 0.1; }           // (9) optional, sensible default
  protected async afterWrite(_tx: Tx, _orders: Order[]): Promise<void> {}   // optional, does nothing

  private validate(rows: unknown[]): { valid: Order[]; errors: RowError[] } {   // (10) private = sealed
    const valid: Order[] = []; const errors: RowError[] = [];
    for (const [i, row] of rows.entries()) {
      const r = orderSchema.safeParse(row);
      r.success ? valid.push(r.data) : errors.push({ line: i + 1, issues: r.error.issues });
    }
    return { valid, errors };
  }
}

export class CsvOrderImporter extends OrderImporter {          // (11) a subclass is now tiny
  protected get name() { return "csv"; }
  protected parse(file: Buffer) { return parseCsv(file); }
}

export class ApiOrderImporter extends OrderImporter {
  protected get name() { return "api"; }
  protected parse(file: Buffer) { return JSON.parse(file.toString()).records; }
  protected errorThreshold() { return 0.25; }                  // (12) this source is known to be messy
  protected async afterWrite(tx: Tx, orders: Order[]) {
    await tx.importAudit.insert({ source: "api", count: orders.length });
  }
}

Now the numbered decisions.

(1) The template method is public, and it must not be overridable.

This is the single most important rule of the pattern. The whole value proposition is that the sequence is guaranteed, and a subclass that can override import can throw the guarantee away. In Java you would mark this final. In C# you simply do not mark it virtual. TypeScript has no final keyword, so the convention is to document it clearly and, if it matters enough, enforce it with a lint rule.

(2) A required hook is declared abstract, so the compiler refuses to let anybody create a subclass that forgot to implement it. This is the pattern's enforcement power in its simplest form: you cannot write an importer that does not parse.

(3) Validation is a fixed step, written once. Notice what this means in practice: every importer, including ones written next year by people who have never read this file, validates rows the same way against the same schema. That consistency is not a convention any more. It is structural.

(4) The error threshold is an optional hook with a default. Most importers want ten percent, so the parent supplies it. An importer whose source is known to be messier overrides it. The alternative designs are both worse: making it a required hook forces every subclass to write return 0.1, and hard-coding it means the messy source cannot be handled at all.

(5) The transaction is a fixed step, and it is the reason the pattern earns its keep here.

Think about what this guarantees. Every importer that will ever exist writes its rows inside a single transaction. A new engineer writing the sixth importer cannot forget, cannot decide it is unnecessary, and cannot get it subtly wrong by opening a transaction in the wrong place. That guarantee is available from Template Method and is genuinely awkward to achieve with Strategy, where the caller assembles the pieces and can assemble them wrongly.

(6) The afterWrite hook is placed deliberately inside the transaction.

That position is itself a design decision that the parent is making on behalf of every subclass. Because afterWrite runs inside the transaction, an audit row written there commits atomically with the data, so you can never have imported rows without their audit record. If the hook were placed after the transaction, that guarantee would silently disappear. The position of a hook is part of its contract, and it should be documented as such.

(7) Cache invalidation happens after the transaction commits, and that ordering is not accidental.

If you cleared the cache before committing, there is a window where another request reads the database, finds the old data, and repopulates the cache with stale values that then survive the commit. That was the bug that got fixed in one of the five copies and not the others. Now it is fixed in the only place it exists.

(8) and (9) show the two hook flavours side by side. Required hooks are abstract and force implementation. Optional hooks have a default and can be ignored. Choosing between them is a judgement about whether a sensible default exists.

(10) validate is private, which seals it. A subclass cannot override it, cannot call it out of order, and cannot skip it. If you later decide that validation should be customisable, you can change private to protected and give it a default. Going in that direction is easy. Going the other way, taking away an extension point that subclasses already rely on, is a breaking change. So start sealed and open up deliberately.

(11) The subclass is now four lines. All the accidental duplication has gone, and what remains is exactly the part that is genuinely different. That is the clearest possible statement of what a CSV importer actually is: an importer that parses CSV.

(12) ApiOrderImporter overrides two hooks, which shows how variation accumulates without disturbing anything else. It has a messier source, so it tolerates more errors, and it wants an audit row, so it uses the hook that runs inside the transaction.

What this does when you run it:

typescript
await new CsvOrderImporter(db, cache, log).import(csvBuffer);
// logs "import starting" → parses CSV → validates → threshold 0.1 → one transaction →
// cache cleared → logs "import done" → { imported: 412, errors: [] }

await new ApiOrderImporter(db, cache, log).import(jsonBuffer);
// identical sequence, but parses JSON, tolerates 25% errors, and writes an audit row
// inside the same transaction as the data

5.1 Hooks: the quiet detail that decides whether the pattern is usable

A hook is an optional step with a default implementation that does nothing, or does something harmless. Hooks are what make a Template Method pleasant to extend rather than painful, and they are the part that inexperienced implementations get wrong.

The failure mode without hooks is that the parent's import method fills up with conditionals such as if (this.needsAudit). Every one of those conditionals means the parent knows something about a specific subclass, which is exactly backwards. The parent should know what kinds of variation exist, and never which subclass does what.

There are three practical rules for hook design.

Give a hook a name that says when it runs, not what a particular subclass does with it. afterWrite is a good name because it describes a position in the sequence. writeAuditRow is a bad name, because it presumes what somebody will do there, and the second subclass that wants a different thing at that point will have to fight the name.

Default to doing nothing, and never to throwing. An optional hook that throws by default is really a required hook with worse how pleasant it is to use, because it turns a compile-time error into a runtime one.

Do not add hooks speculatively. Every hook is a promise about your internal sequence that you can never take back, because subclasses will depend on it. Add a hook when a second subclass genuinely needs it, not when you imagine one might.

5.2 Python: the same pattern, plus context managers

python
from abc import ABC, abstractmethod

class OrderImporter(ABC):
    def import_orders(self, data: bytes) -> ImportResult:     # the template method
        started = time.perf_counter()
        rows = self.parse(data)                               # required hook
        valid, errors = self._validate(rows)                  # fixed step (leading _ = sealed by convention)
        if len(errors) > len(rows) * self.error_threshold():  # optional hook
            raise TooManyErrors(errors)
        with self.db.transaction() as tx:                     # fixed step
            for order in valid:
                tx.orders.upsert(order)
            self.after_write(tx, valid)                       # optional hook, inside the transaction
        self.cache.invalidate("orders")
        return ImportResult(len(valid), errors)

    @abstractmethod
    def parse(self, data: bytes) -> list: ...                 # subclasses MUST implement

    def error_threshold(self) -> float: return 0.1            # subclasses MAY override
    def after_write(self, tx, orders) -> None: pass           # subclasses MAY override

Python's ABC and @abstractmethod give you the same compile-time-ish guarantee: instantiating a subclass that has not implemented parse raises a TypeError immediately rather than failing somewhere deep inside the import.

Python also offers a genuinely different and often better tool for the setup-and-cleanup half of this problem, which is the context manager. When the only thing you are enforcing is "always do this before and this after", with expresses it more directly than inheritance does, and it composes, which inheritance does not:

python
@contextmanager
def import_session(db, cache, log):
    started = time.perf_counter()
    log.info("import starting")
    with db.transaction() as tx:
        yield tx                                  # the caller's code runs here
    cache.invalidate("orders")                    # always runs afterwards
    log.info("import done in %.1fms", (time.perf_counter() - started) * 1000)

The equivalent in other languages is a higher-order function that takes a callback, and it is worth recognising that this is Template Method expressed with composition instead of inheritance. Section 6.2 develops that idea properly.

6. Going deeper

6.1 The Hollywood Principle, and why it matters more than it sounds

There is a slogan attached to this pattern: "Don't call us, we'll call you." It is sometimes called the Hollywood Principle, and behind the joke there is a genuinely important idea about control.

In ordinary code that you write, your code is in charge. It calls a library, gets a result, calls another library, and decides what happens next. The library is a passive tool.

In a Template Method, that relationship is inverted. The parent is in charge of the sequence, and it calls down into your subclass at moments it chooses. Your code is now the passive part being called.

That inversion is exactly what a framework is. When you write a React component, you do not call React; React calls your render. When you write a JUnit test, you do not run the test runner; the runner calls your @Test method, having already called @Before. When you write an Express error handler, you do not invoke it; Express does, at the moment it decides.

This is why Template Method is the dominant pattern inside frameworks even though Strategy is usually better inside applications. A framework's entire job is to own a sequence and let you plug into it. A framework that handed you the pieces and asked you to assemble them would not be providing much.

The practical consequence for you as a library author: if you are writing something that owns a lifecycle, Template Method is probably right. If you are writing application code where the caller owns the flow, Strategy is probably right.

6.2 The composition version, which is usually the better default

Almost every Template Method can be rewritten using functions instead of inheritance, and in application code that rewrite is usually an improvement. Here is the same importer with no classes at all:

typescript
type ImportSteps = {
  name: string;
  parse: (file: Buffer) => unknown[];                  // (1) the required step, as a function
  errorThreshold?: number;                             // (2) the optional step, as an optional value
  afterWrite?: (tx: Tx, orders: Order[]) => Promise<void>;
};

export function makeImporter(steps: ImportSteps, deps: Deps) {    // (3) the skeleton is a closure
  return async function importOrders(file: Buffer): Promise<ImportResult> {
    const started = Date.now();
    const rows = steps.parse(file);                    // the hole, filled by a function
    const { valid, errors } = validate(rows);          // still shared, still written once
    if (errors.length > rows.length * (steps.errorThreshold ?? 0.1)) throw new TooManyErrors(errors);
    await deps.db.transaction(async (tx) => {
      for (const o of valid) await tx.orders.upsert(o);
      await steps.afterWrite?.(tx, valid);
    });
    await deps.cache.invalidate("orders");
    return { imported: valid.length, errors };
  };
}

const importCsv = makeImporter({ name: "csv", parse: parseCsv }, deps);
const importApi = makeImporter({ name: "api", parse: parseApiJson, errorThreshold: 0.25 }, deps);

Look at what this version keeps and what it gives up.

It keeps everything that mattered. The sequence is still written once. The transaction is still guaranteed. A caller still cannot skip validation or reorder the steps, because the skeleton function controls all of that.

It gains several things. The steps can be chosen at runtime rather than at compile time, so an importer can be built from a configuration file. The steps can be tested individually as plain functions, with no subclass to construct. Steps can be reused across different skeletons, since parseCsv is not tied to any hierarchy. There is no fragile base class problem, because there is no base class. And reading importCsv requires opening one function rather than mentally merging a parent and a child.

It gives up two things. The compiler no longer forces you to supply a required step in quite the same way, although making the field non-optional in the type recovers most of that. And there is no protected visibility, so a step function cannot reach into the skeleton's internals — which, honestly, is usually a benefit rather than a loss.

The recommendation, stated plainly: in application code, prefer the composition version. Reach for inheritance-based Template Method when you are building a framework whose users expect to subclass, when your language or ecosystem strongly favours it, or when you genuinely need a deep hierarchy of shared behaviour. Do not reach for it merely because you learned it from a book that was written in 1994, when inheritance was the only tool available.

6.3 The fragile base class problem, explained properly

This is the concrete reason to be cautious with inheritance, and it is worth understanding rather than repeating as a slogan.

The problem arises because a parent calling its own overridable methods creates a hidden two-way dependency. The child depends on the parent, which is expected. But the parent now also depends on what the child does inside the methods it overrode, which is not visible anywhere in the parent's code.

Here is a concrete example of how that bites:

typescript
class Base {
  protected items: string[] = [];
  add(item: string) { this.items.push(item); }
  addAll(items: string[]) {
    for (const i of items) this.add(i);          // ← the parent calls its own overridable method
  }
}

class Counting extends Base {
  count = 0;
  add(item: string) { this.count++; super.add(item); }
  addAll(items: string[]) { this.count += items.length; super.addAll(items); }   
}

const c = new Counting();
c.addAll(["a", "b"]);
console.log(c.count);    // 4, not 2 — counted once in addAll, then again in each add

The subclass author was not careless. They had no way of knowing that addAll was implemented in terms of add, because that is an internal detail of the parent. And the bug direction can reverse without warning: if a later version of Base optimises addAll to push directly instead of calling add, then Counting starts under-counting instead of over-counting, and nothing in the subclass changed.

The lesson is that when a class is designed for inheritance, which methods call which other methods becomes part of its public contract, and changing that wiring is a breaking change even though no signature changed. Josh Bloch's advice in Effective Java is the standard formulation: design and document for inheritance, or prohibit it.

Practically, that means three things. Document, for every overridable method, whether the template method or any other method calls it, and when. Prefer to make the fixed steps private so they cannot participate in this problem at all. And when in doubt, use composition, where this entire category of bug does not exist because there is no inverted call direction.

6.4 Keeping the hierarchy shallow

Template Method invites depth, and depth is where it becomes unmaintainable. The progression is predictable: BaseImporter, then FileImporter extends BaseImporter, then DelimitedFileImporter extends FileImporter, then CsvImporter extends DelimitedFileImporter.

By the fourth level, answering "what does CsvImporter.import() actually do?" requires reading four files and tracking which level overrode which step. Debugging means stepping through four stack frames that all have the same method name.

Two rules keep this under control. Cap the hierarchy at two levels, meaning one abstract parent and concrete children, and treat a third level as a design review trigger rather than a normal event. And when you feel the pull toward a third level, extract the shared behaviour into a collaborator object that both children hold, rather than into another parent class. That converts an inheritance problem into a composition one, which is a much better problem to have.

7. Where you would actually use this

(a) Frameworks with lifecycle methods. This is the pattern's home ground. React class components with componentDidMount and render, Android Activity with onCreate and onResume, Java servlets with doGet and doPost, iOS UIViewController. In every case the framework owns the sequence and calls your overrides at the right moments.

(b) Test frameworks. JUnit, pytest and Jest all run a fixed sequence around your test: set up, run, tear down, report. beforeEach and afterEach are hooks in a template method, and the guarantee that teardown runs even when the test throws is exactly the kind of rule a parent enforces on its children.

(c) Data processing pipelines. Extract, transform and load, where the shape is always the same and only the extraction and the transformation vary. This is the importer story from section 1, and it is extremely common in reporting and integration systems.

(d) Request handling in web frameworks. A base controller that authenticates, deserialises, calls your handler, serialises the result and handles errors, calling into your method in the middle. Rails controllers, Django class-based views and Spring's AbstractController all work this way.

(e) Build and deployment tools. Maven's build lifecycle with its fixed phases of validate, compile, test, package, verify, install and deploy, where plugins bind to phases. The order is the product, and it is deliberately not configurable.

(f) Game loops. Initialise, then repeatedly handle input, update state and render, then clean up. Every game engine offers this as a fixed loop with overridable update and render methods, because getting the order wrong produces visibly broken behaviour.

(g) Document and report generation. A generator that writes a header, then the body, then a footer, where subclasses supply the body content and optionally customise the header. The structural guarantees, such as a footer always being present, are what the parent is protecting.

(h) Cryptographic and protocol operations. Sequences where the order is a security requirement rather than a preference, such as generating a nonce, encrypting, and then computing a MAC over the ciphertext. Encrypt-then-MAC is a genuine security property, and a template method is a reasonable way to make sure nobody reorders it.

8. Variants

VariantWhat it looks likeNotes
Classic Template Methodabstract parent, concrete childrenthe Gang of Four form
Hook-heavy templatemany optional steps with defaultsflexible; risks a bloated parent
Functional templatea skeleton function taking step functionsthe recommended default in application code (section 6.2)
Higher-order functionwithTransaction(fn), withRetry(fn)a template method for one wrapping concern
Context manager / usingwith blocks, try-with-resourcesthe language's own template method for setup and cleanup
Template Method plus Strategyfixed skeleton, injected stepskeeps the sequence, gains runtime swapping
Abstract class with a factory stepone hook creates the object used laterthis is Factory Method living inside a Template Method

The last row deserves a note, because it explains a relationship that confuses people. Factory Method is very often just a Template Method whose overridable step happens to be object creation. The parent runs a sequence and, at one point, needs an object it cannot name, so it calls protected abstract createX(). That is why the two patterns feel so similar in the original book: structurally, one is a special case of the other.

9. Where you already use it

What you have usedThe fixed sequenceThe hole you fill
try / finallyrun the body, then always clean upthe body
A test runnerset up, run, tear down, reportthe test body
A game loopread input, update, draw, repeatupdate and draw
A build pipelinecheck, compile, package, publishwhat happens at each stage
items.sort(fn)the sorting algorithmthe comparison, passed as a function

try/finally is the smallest possible version, and seeing it as this pattern makes the point quickly. The language owns the sequence: run the body, then run the cleanup no matter what happened — whether the body finished normally, returned early, or threw. You own only the body and the cleanup. You cannot reorder those two, you cannot skip the cleanup, and you cannot make it run first. That is what "fixed sequence with holes you fill" means.

Notice too what the guarantee buys. Because the cleanup cannot be skipped, you no longer have to remember to close the file on every path out of the function. Getting it wrong is not possible, rather than merely discouraged. Every good use of this pattern has that shape: the parent enforces the step nobody must forget.

10. Ways to get it wrong

  1. An overridable template method. If a subclass can override the sequence, the guarantee is gone and you have inheritance with extra ceremony.

    The fix: make it final, non-virtual, or enforced by a lint rule.

  2. Too many hooks. A parent with fifteen extension points has no real skeleton left, and every subclass has to understand all fifteen.

    The fix: add hooks when a second subclass genuinely needs them, never speculatively.

  3. A deep hierarchy. Four levels of inheritance where understanding one class means reading four files.

    The fix: cap at two levels, and extract shared behaviour into a collaborator instead of another parent.

  4. The parent knowing about specific subclasses. Lines like if (this instanceof CsvImporter) invert the whole design.

    The fix: whatever that condition was checking should be a hook.

  5. Fragile base class breakage. The parent calls its own overridable methods, so a subclass double-counts or skips work (section 6.3).

    The fix: document the call structure as part of the contract, prefer private fixed steps, or use composition.

  6. Using it for runtime variation. Choosing behaviour by subclass when the choice actually needs to be made at runtime.

    The fix: Strategy, or the functional version.

  7. Steps that need very different inputs. One subclass's step needs three extra parameters that the others ignore, so the hook signature bloats.

    The fix: the same four options as Strategy section 6, usually giving that subclass its own collaborator.

  8. Inheriting purely to reuse a couple of helpers. Extending a class for its utility methods rather than for the sequence.

    The fix: extract the helpers into a module or a collaborator, and do not inherit.

  9. Hooks that throw by default. That makes them required hooks with worse how pleasant it is to use, since the failure moves from compile time to runtime.

    The fix: required hooks are abstract; optional hooks do nothing.

  10. Undocumented hook positions. A subclass author cannot tell whether afterWrite runs inside the transaction.

The fix: document the position, and test it.

11. Template Method compared with its neighbours

Compared withThe differenceChoose Template Method when
StrategyStrategy replaces the whole algorithm through composition at runtime. Template Method fills in steps through inheritance at compile timethe sequence must be guaranteed and only a few steps vary
Factory Methoda Factory Method is usually a Template Method whose one variable step is creating an objectthe variation is broader than object creation
Decoratora Decorator wraps behaviour from outside without knowing its internals. Template Method opens named holes from insideyou own the algorithm and want controlled extension points
Chain of Responsibilitya Chain is an ordered list of independent handlers, composed at runtime. Template Method is one fixed sequence with fixed holesthe steps are parts of one algorithm, not separate concerns
Higher-order functionthe same idea with no class, no hierarchy, and runtime compositionyou need a hierarchy, or your framework's users expect to subclass

Template Method versus Strategy is the comparison that matters most, because in most application code you have a real choice between them.

The clearest way to hold the difference is to ask what is being protected. Template Method protects the order, and it lets you vary the steps. Strategy protects the question, and it lets you vary the whole answer.

The second difference is when the choice is made. Template Method binds at compile time through inheritance, so one class is one variant forever. Strategy binds at runtime through composition, so the same object can be reconfigured, tested with a fake, or driven from configuration.

The third difference, and the practical one, is that strategies compose and subclasses do not. You can combine three strategies. You cannot combine three subclasses.

The interview sentence: "Template Method fixes the order and varies the steps, using inheritance at compile time. Strategy fixes the question and varies the whole algorithm, using composition at runtime. If I need runtime swapping or combination, I use Strategy — and in application code I usually write the template as a function taking step functions, which keeps the guaranteed sequence without the inheritance."

12. Interview calibration

The 45-second answer, in the order you would say it:

Template Method puts the fixed sequence of an algorithm in a parent class and leaves named holes for the steps that vary, which subclasses fill in. The trigger is several functions doing the same six things in the same order where only one or two steps differ, so bug fixes and new steps have to be applied five times and inevitably drift.

What the parent buys you is enforcement: every importer writes inside a transaction and clears the cache after commit, and a new subclass cannot forget, because those steps are not overridable. I distinguish three kinds of step — fixed and private, required and abstract, optional with a sensible default — and I document where each hook runs, because a hook that runs inside the transaction has a different meaning from one that runs after it.

The costs are real: it is inheritance, so you get the fragile base class problem where the parent calls its own overridable methods, one class can only ever be one variant, and hierarchies grow deep. So in application code I usually write the skeleton as a function that takes the step functions, which keeps the guaranteed sequence and gains runtime composition and easier testing.

I reach for the inheritance version mainly when writing a framework, where owning the lifecycle and calling into user code is the whole point.

Follow-up questions, with the seed of each answer:

  • "Template Method versus Strategy?" — Template Method fixes the order and varies steps, by inheritance at compile time. Strategy fixes the question and varies the whole algorithm, by composition at runtime.
  • "What is the fragile base class problem?" — The parent calls its own overridable methods, so which method calls which becomes part of the contract, and changing that wiring breaks subclasses without any signature changing.
  • "Why are frameworks full of this pattern?" — Because a framework owns the lifecycle and calls into your code, which is the Hollywood Principle. That inversion is what makes it a framework rather than a library.
  • "How do you design the hooks?" — Required hooks are abstract, optional hooks have a do-nothing default, names describe when they run rather than what a subclass does, and hooks get added when a second subclass needs one.
  • "How deep should the hierarchy go?" — Two levels. A third is a design review trigger, and the fix is a collaborator rather than another parent.
  • "Can you do this without inheritance?" — Yes, and usually you should. A skeleton function taking step functions keeps the sequence guaranteed while gaining runtime choice, individually testable steps and no base class.

Recall

  • Template Method means writing the sequence once in a parent and leaving named holes for the steps that vary. The trigger is several functions doing the same steps in the same order, where only one or two differ, so fixes have to be applied many times and drift apart.
  • How you arrive at it: what varies is a few individual steps, and what stays fixed is the order of the steps and the rules around them. That is the exact opposite of Strategy, where the whole algorithm varies. The choice is bound at compile time, by writing a subclass.
  • The template method itself must not be overridablefinal, non-virtual, or lint-enforced. If a subclass can replace the sequence, the guarantee that justified the pattern is gone.
  • Three kinds of step, and getting them right is most of the design: fixed steps are private and cannot be touched; required hooks are abstract so the compiler forces implementation; optional hooks have a sensible default that does nothing.
  • A hook's position is part of its contract. A hook inside the transaction commits atomically with the data; the same hook after the transaction does not. Document the position and write a test for it.
  • The enforcement power is the real reason to choose this over Strategy: every subclass, including ones written next year, writes inside a transaction and clears the cache after commit, and cannot forget.
  • The Hollywood Principle — "don't call us, we'll call you" — is why frameworks are full of this pattern. The framework owns the lifecycle and calls into your code, which is what makes it a framework rather than a library.
  • The costs are heavy: inheritance coupling, the fragile base class problem (the parent calls its own overridable methods, so the call wiring becomes part of the public contract), one class can only be one variant, reading requires jumping between files, and hierarchies grow deep.
  • In application code, prefer the composition version — a skeleton function taking step functions. It keeps the guaranteed sequence and gains runtime choice, individually testable steps, reusable steps, and no base class. Reach for inheritance mainly when writing a framework.

Self-test: Why must the template method itself be non-overridable? Name the three kinds of step and how each is declared. Why is a hook's position part of its contract? Explain the fragile base class problem with an example. Give the one-sentence Template-Method-versus-Strategy answer.

Quiz Bank

FoundationalShow how Template Method is derived from five near-identical importers, and state exactly what the copy-paste version costs.

The naive starting point is writing each variant as its own complete function, which is correct when the variants are genuinely different or when there are only two of them sharing very little.

The force is that several procedures follow the same steps in the same order, only a small number of those steps differ, and the order itself is a meaningful rule rather than an accident.

What the copy-paste version costs. First, a change to the shared sequence must be made five times, so somebody adding metrics updates three importers and misses two, and the two silently have no metrics forever, because missing metrics do not throw. Second, bug fixes drift apart: somebody discovers the cache must be cleared after the transaction commits rather than before, fixes it in the importer where it was reported, and leaves the same bug in four others to be rediscovered separately over the following years. Third, nobody can see the shared algorithm, because the sequence of parse, validate, threshold, transaction, cache and log is a real business rule that exists in five places and is written down in none. Fourth, adding a sixth importer means copying a fifth, so whichever file you copy carries whatever drift it has accumulated, and the copies get worse rather than better. Fifth, nothing can be enforced, since there is no way to guarantee a new importer wraps its writes in a transaction or checks the threshold.

Drawing the line: a few individual steps vary, while the order of the steps and the rules around them stay fixed. That is the opposite orientation from Strategy, where the whole algorithm varies.

When the choice is made: at compile time, by writing a subclass, which is the earliest binding of any pattern in this catalogue.

The resulting pattern is a public, non-overridable template method holding the sequence, private fixed steps, abstract required hooks, and optional hooks with sensible defaults. Each subclass then shrinks to the few lines that are genuinely different.

What the pattern costs in return: inheritance coupling with the fragile base class problem, one class being permanently one variant, reading requiring you to merge parent and child mentally, and a standing invitation to grow deep hierarchies.

FoundationalExplain the three kinds of step in a template method, and why the position of a hook is part of its contract.

There are three kinds of step, and choosing correctly between them is most of the design work.

A fixed step is declared private in the parent. It always runs, in its fixed position, and no subclass can override it, call it out of order or skip it. Validation and the transaction wrapper are fixed steps in the importer example. Fixed steps are where the pattern's enforcement power lives.

A required hook is declared abstract and protected. Every subclass must supply it, and the compiler refuses to allow a subclass that forgot. Parsing is a required hook, because there is no sensible default for "how do you read this file format" and an importer that does not parse is not an importer.

An optional hook is declared protected with a working default. Subclasses may override it, and most will not. The error threshold is an optional hook because ten percent suits nearly everybody, and forcing every subclass to write return 0.1 would be pure boilerplate.

Getting these wrong produces specific pains. A required hook that should have had a default forces boilerplate into every subclass. An optional hook that should have been required lets a subclass silently do nothing when it should have done something important.

Why a hook's position is part of its contract. Consider afterWrite in the importer. Placed inside the transaction, an audit row written by that hook commits atomically with the imported data, so it becomes impossible to have imported rows without a matching audit record. Move that same hook to just after the transaction and the guarantee vanishes silently: the audit row is now a separate write that can fail on its own, and nothing in the subclass changed to cause it.

That means the position is a promise the parent makes to its subclasses, exactly like a method signature is. A subclass author who put a database write in afterWrite was relying on that promise whether they articulated it or not.

Three practical consequences follow. Document the position of every hook, stating plainly whether it runs inside a transaction, before or after a commit, and whether it may throw. Write a test that proves the position, such as making the hook fail and asserting the data was rolled back. And treat moving a hook as a breaking change requiring the same care as changing a signature, because subclasses depend on it and the compiler will not catch the difference.

AppliedExplain the fragile base class problem with a concrete example, and give the practical rules for avoiding it.

The problem comes from a hidden two-way dependency. A child depending on its parent is expected and visible. But when the parent calls its own overridable methods, the parent now also depends on what the child does inside those methods, and that dependency appears nowhere in the parent's code.

A concrete example. A Base class has add(item) and addAll(items), where addAll is implemented by looping and calling this.add for each item. A subclass called Counting overrides both, incrementing a counter in each. Now calling addAll(["a","b"]) gives a count of four rather than two, because the counter was incremented once for the batch in addAll and then again for each item when super.addAll looped through add.

The subclass author was not careless. They had no way to know that addAll was implemented in terms of add, since that is an internal detail of the parent that no signature reveals.

The direction of the bug can also reverse without warning. Suppose a later version of Base optimises addAll to push directly into the array instead of calling add. Counting now under-counts instead of over-counting, and nothing in the subclass changed. A performance improvement in the parent silently broke a subclass in a different repository.

The lesson is that when a class is designed to be inherited from, which methods call which other methods becomes part of its public contract. Changing that internal wiring is a breaking change even though every signature stayed the same, which is why this problem is genuinely nastier than ordinary coupling.

The practical rules. First, document the call structure for every overridable method: state whether the template method calls it, whether any other overridable method calls it, and at what point in the sequence. Second, prefer to make fixed steps private, because a private method cannot be overridden and therefore cannot participate in this problem at all. Third, keep the number of overridable methods small, since each one is another piece of contract you can never change. Fourth, follow the standard advice from Effective Java: design and document a class for inheritance, or explicitly prohibit inheritance by making it final. A class that is accidentally inheritable is the worst case, because its author never considered the contract at all.

And the strongest rule: when in doubt, use composition. In the functional version of Template Method, the skeleton calls step functions that were passed in from outside, so there is no inverted call direction, no super, and this entire category of bug simply does not exist.

InterviewYou are designing a data-processing framework where third-party teams write plugins. Compare Template Method and Strategy for this, and design the extension model.

This is one of the few situations where Template Method is genuinely the stronger choice, and the reason is enforcement combined with the direction of control.

Why Template Method fits. A framework's whole job is to own a lifecycle and call into user code at defined moments, which is the Hollywood Principle: don't call us, we'll call you. If you handed plugin authors the individual pieces and asked them to assemble the pipeline themselves, you would not have a framework, and every plugin would assemble it slightly differently. More importantly, there are rules you must be able to guarantee regardless of what a third party writes: that a plugin's writes happen inside a transaction, that resources are released even when the plugin throws, that a timeout is applied, that metrics are recorded, and that failures are reported in a consistent shape. Those guarantees are exactly what a non-overridable template method provides.

Why Strategy alone is not enough here. With pure Strategy, the caller composes the steps, which means a plugin author could compose them wrongly, skip the transaction, or forget the cleanup. You would be relying on documentation and review to enforce properties that should be structural.

The design, which uses both. The framework owns a sealed pipeline: acquire resources, start a timer and a span, validate the plugin's declared configuration against a schema, call the plugin's extract, call its transform, write inside a transaction, call an afterCommit hook, release resources in a finally, and record metrics and outcome. The template method is final. Inside it, the extension points are declared explicitly.

Then, and this is the important refinement, the extension points themselves are Strategy-shaped rather than inheritance-shaped. A plugin does not subclass your pipeline. It registers an object or a set of functions that satisfy a published interface. That gives you the best of both: the framework keeps its guaranteed sequence, while plugin authors get objects they can unit-test without constructing your framework, and you avoid exposing your internals through protected members that become an accidental contract.

Practical rules for the extension model. Publish a small interface with a version, because plugins are compiled separately and will lag your releases. Make required steps genuinely required in the type, and give optional steps defaults so a minimal plugin is a few lines. Name hooks by when they run, never by what you imagine a plugin will do there. Document the position of each hook precisely, especially whether it runs inside the transaction, because plugin authors will depend on it. And keep the number of hooks small, adding one only when a second real plugin needs it, since every hook is a promise about your internals that you can never withdraw.

Isolation, which matters more for third-party code than for your own. Apply a timeout and a memory or CPU budget to each plugin call, catch and classify plugin exceptions so one bad plugin cannot take down a run, and record which plugin failed so support can route the problem to the right team. Consider running untrusted plugins in a separate process or a sandbox. A framework that lets one plugin's infinite loop hang the whole pipeline will be blamed for that plugin's bug.

Evolution. Adding a new optional hook is backwards-compatible. Adding a required one is not, so it needs a version bump and a migration window where the old interface still works. Removing or moving a hook is a breaking change even if nothing about its signature changed, for exactly the fragile-base-class reasons discussed earlier.

The summary sentence: own the sequence with a sealed template method so the guarantees are structural rather than documented, but expose the extension points as small versioned interfaces that plugins implement by composition rather than by subclassing, and isolate every plugin call with timeouts and error classification, because in a framework the sequence is your product and third-party code is the part you cannot trust.

StaffA codebase has a five-level inheritance hierarchy of report generators. Adding a report means picking the right parent, and nobody can predict which methods will run. Plan the untangling.

The diagnosis is that inheritance was used for code reuse rather than for a genuine shared sequence. Each level was added by somebody who found most of what they needed one level up and added the rest, which is a locally reasonable decision that compounds into an unmaintainable structure. The symptom of "nobody can predict which methods will run" is the fragile base class problem at scale: with five levels, any method call may resolve to any of five implementations, and the parent calling its own overridable methods means the effective behaviour of a leaf class is a weave of all five.

Step one is to map reality before changing anything. Generate the actual override matrix: every class down the side, every overridable method across the top, and a mark where each class overrides. This single artefact usually reveals the truth immediately. Typically you find that most methods are overridden at only one level, meaning the intermediate levels exist for one or two methods each, and that a handful of methods are overridden everywhere, meaning they were never really shared behaviour at all.

Step two is to separate the genuine sequence from the incidental sharing. Ask, for each thing the hierarchy provides, whether it is ordering or reuse. Ordering means "these steps must happen in this order", which is a real Template Method concern. Reuse means "several classes happen to need this helper", which is not, and which should have been a module or a collaborator object from the beginning. In a five-level hierarchy, most of the middle levels turn out to be reuse dressed up as inheritance.

Step three is to define the target: one skeleton, at most two levels, with everything else as collaborators. There is one report-generation sequence, expressed once. Everything that varies becomes an injected step or a collaborator object. Formatting becomes a Formatter collaborator, data access becomes a DataSource, layout becomes a Layout. A specific report is then a configuration of those collaborators rather than a position in a tree. That change alone converts "pick the right parent, and hope" into "declare what this report uses", which is something a newcomer can do correctly on their first day.

Step four is the migration, and it must be incremental because reports are usually business-critical and numerous. Start by pinning behaviour with golden-file tests: run every existing report against fixed inputs and store the outputs, because those outputs, bugs included, are the specification. Then take the leaf classes first rather than the root, since leaves have no dependants and can be converted one at a time with zero blast radius. For each leaf, write it in the new composition style, run it against its golden file, and delete the old class. As leaves are removed, intermediate classes lose all their subclasses and become deletable, so the hierarchy collapses from the bottom upward. That order matters: attacking the root first means every change touches everything.

Step five is to prevent regrowth, because a hierarchy that was untangled once will regrow if the incentives do not change. Add a lint rule capping inheritance depth. Require that any new report is created by configuring collaborators, and make that the path of least resistance by providing a builder or a factory that makes it genuinely easier than subclassing. And write down the rule about when inheritance is acceptable at all, so the next engineer has something to point at.

What to measure: the maximum inheritance depth trending toward two, the number of classes needed to understand one report trending toward one, the time for a new engineer to add a report, and golden-file differences remaining at zero throughout the migration, which is your proof that behaviour never changed.

One honest caveat worth raising. If the reports genuinely do share a strict sequence that must be guaranteed, do not throw the template method away along with the hierarchy. Keep one skeleton, either as a single abstract parent with concrete children, or better as a skeleton function taking step functions. The problem was never that a shared sequence existed. The problem was five levels of inheritance used for reuse that a collaborator would have provided better.

Flashcards

FlashTemplate Method in one line

The parent writes the sequence once and leaves named holes; subclasses fill only the steps that differ. The order is what is being protected.

FlashTemplate Method versus Strategy

Template Method fixes the order and varies steps, by inheritance at compile time. Strategy fixes the question and varies the whole algorithm, by composition at runtime. Strategies compose; subclasses do not.

FlashThe three kinds of step

Fixed steps are private and unoverridable. Required hooks are abstract, so the compiler forces implementation. Optional hooks have a default that does nothing.

FlashWhy a hook's position matters

A hook inside the transaction commits atomically with the data; the same hook after it does not. Position is a promise to subclasses, so document it and test it.

FlashThe fragile base class problem

The parent calls its own overridable methods, so which method calls which becomes part of the public contract. Changing that wiring breaks subclasses with no signature change.

FlashThe Hollywood Principle

Don't call us, we'll call you. The parent owns the sequence and calls down into your code. That inversion of control is exactly what makes something a framework rather than a library.

Scenario Drill

DrillDesign the job-execution core of a background worker system: every job must be timed, retried with backoff, logged with a correlation ID, run under a timeout, report metrics, and end in a terminal state — while teams across the company write their own job types. Show where Template Method belongs, where it does not, and the failure modes.

This is a strong fit for Template Method, and the reason is that the list of requirements in the question is a list of guarantees. Every job must be timed, retried, logged, bounded by a timeout, measured and finished. Guarantees that must hold across code written by many teams are exactly what a sealed sequence provides, and exactly what documentation and code review fail to provide reliably.

The sealed skeleton. The worker core owns this sequence and none of it is overridable: pull the job, deserialise and validate the payload against the job type's declared schema, establish a correlation identifier and a tracing span, check whether this job has already been processed by its idempotency key, start a timer, run the job body under a hard timeout, classify the outcome, decide retry or dead-letter, write the terminal state, emit metrics, and release resources in a finally block that runs no matter what happened.

Note how many separate incidents that sequence prevents. A job author cannot forget the timeout, which means one badly written job cannot occupy a worker slot forever. They cannot forget to mark the job finished, which means jobs cannot silently vanish. They cannot skip the idempotency check, which means an at-least-once delivery cannot double-charge a customer. And they cannot swallow their own errors in a way that hides a failure from the metrics, because classification happens outside their code.

The holes, and their kinds. The required hook is run(payload, ctx), which is the job body itself and has no sensible default. Optional hooks with defaults include maxAttempts() defaulting to something like five, backoff(attempt) defaulting to exponential with jitter, timeout() defaulting to a conservative value, isRetryable(error) defaulting to a sensible classification of transient versus permanent, and onFinalFailure(job, error) defaulting to doing nothing beyond the standard dead-letter write.

isRetryable deserves special attention, because it is the hook that most affects correctness. The default should classify network and timeout errors as retryable and validation errors as permanent, since retrying a malformed payload forever is pure waste. Job authors override it when their domain has specific knowledge, such as a payment gateway error code that means "retry later" rather than "declined".

Where Template Method does not belong here. Do not make job authors subclass a base class from your worker library. That is the mistake this design is most likely to fall into. Instead the core uses a template method internally, while job types are registered as objects or functions satisfying a small published interface. Job authors then get something they can unit-test in isolation, with no worker framework to construct, and you avoid exposing your internals as protected members that quietly become a contract you can never change. This is the framework rule from section 6.1 combined with the composition preference from section 6.2: own the sequence, but expose the extension points as composition.

Also do not put business logic into the skeleton. If the core starts containing if (job.type === "email"), the design has inverted and the core now knows about specific jobs. Anything that varies per job type must be a declared property of that job type, not a condition in the core.

The failure modes to design against.

A job that ignores its timeout. A hard timeout in the core cannot actually interrupt arbitrary synchronous code in most runtimes. So the timeout must be enforced at a level that works: a cancellation signal passed to the job for cooperative cancellation, plus a supervisor that can kill the worker process if a job exceeds a hard limit. Say this out loud in an interview, because claiming a timeout works when it cannot is a common mistake.

Retries that duplicate side effects. The core provides the idempotency key and the deduplication check, but the job body must still be written so that repetition is harmless, and the framework should make that easy by giving jobs a transactional context that includes the processed-job record.

Poison messages. A job that crashes the worker will be redelivered and crash it again, taking down throughput for everything. Cap attempts, quarantine after repeated crashes, and alert on the dead-letter queue, because an unwatched dead-letter queue is a silent data-loss bucket.

Payload version skew. A job enqueued by today's deploy may execute on next week's code, so payloads carry a version and handlers must tolerate the previous shape for at least one deploy cycle.

Hook creep. Every team will ask for one more extension point. Resist, and add a hook only when a second team genuinely needs the same one, because each hook is a permanent promise about your internal sequence.

The summary sentence: the worker core is a sealed template method because the requirements are guarantees that must hold across code written by teams you do not control, but the job types plug in as small composed objects rather than subclasses, the extension points are a handful of well-named hooks with safe defaults, and the hard parts are the ones no pattern solves for you — real timeout enforcement, idempotent bodies under at-least-once delivery, and poison-message containment.