skip to content

What are the main drawbacks and failure modes of the Template Method pattern, and how would you refactor away from it when they start to hurt?

level: seniorimportance: should knowfreq 40%

answer

  1. fragile base class = white-box reuse
  2. bound at construction, one axis only
  3. adding an abstract step breaks subclasses
  4. yo-yo problem across the hierarchy
  5. Replace Inheritance with Delegation

basics

~20 s

It relies on inheritance, so the variant is fixed when the object is created, only one axis can vary, and subclasses depend on the base class's internals — a base-class change can break them. When it hurts, move the varying steps into injected objects or functions (Strategy) instead of subclasses.

solid answer

~50 s

Four recurring pains. (1) Fragile base class: subclasses depend on the skeleton's internals, so changing step order, adding a step, or altering when a hook fires silently breaks code you may not own. (2) Rigidity: the variant is bound at construction, so no runtime swapping, and single inheritance means independent axes of variation cause a combinatorial subclass explosion. (3) Comprehension cost: reading one algorithm means jumping across a hierarchy — deep chains where each level overrides a bit are notoriously hard to debug. (4) LSP violations: an override that throws where the skeleton assumes success, or mutates skeleton-owned state, breaks substitutability with no compile-time signal. The standard escape is "Replace Inheritance with Delegation": promote each varying step to a small interface (or a function parameter), inject it into a now-concrete class, and keep the skeleton as a final method that delegates. Migrate incrementally — keep the abstract class as a thin adapter that forwards to the new collaborators, deprecate, then delete.

go deeper

for a junior

Name two concrete drawbacks: you can't change the step at runtime, and subclasses break if the base class changes.

for a middle

Add single-inheritance/class-explosion, testing friction, and the basic escape route: move the varying step into an injected object.

for a senior

Discuss fragile base class and self-use, LSP violations with no compile-time signal, evolution rules for a published base class, and a concrete incremental migration recipe.

for a principal

Frame it as extension-surface governance: what you publish you must support; prefer narrow injected interfaces or middleware for third-party extension, define deprecation policy for steps, and know when the hierarchy is still the cheaper answer.

## Where Template Method earns its keep A quick reminder so the criticism lands in context: a base type holds a fixed algorithm (the *template method*) and calls steps that subtypes override — abstract *primitive operations* (must override) and *hooks* (may override). It removes duplicated control flow and lets a framework own a lifecycle. That is real value; the problems below are about *scale and evolution*, not about the pattern being wrong. ## Failure mode 1 — the fragile base class problem Inheritance is *white-box reuse*: the subclass sees and depends on the base class's internals — which steps exist, in what order they are called, what state is set by the time each one runs, whether one step calls another. None of that is captured in a type signature, so: - Reordering two steps, or calling a step twice instead of once, can break subclasses while the code still compiles. - Adding a new **abstract** step to a released base class is an outright breaking change: every existing subclass stops compiling. - Even adding a *defaulted* hook can break behaviour if the default does something a subclass did not expect. - A base-class method that internally calls another overridable method creates *self-use* dependencies: an override changes behaviour the base class relies on. (This is why the classic guidance is "design and document for inheritance, or else prohibit it.") The blast radius grows with the number of subclasses — and if the base class is published to other teams or the public, you cannot even enumerate them. ## Failure mode 2 — rigidity and class explosion - **Bound at construction.** Choosing a variant means instantiating a specific subclass; you cannot change the step later on the same object. - **One axis only.** With single inheritance, two independent varying aspects (format × destination, retry policy × serialisation) force a subclass per combination: 3 × 3 = 9, and each new value multiplies again. - **No reuse of the step elsewhere.** The overridden method is welded to that hierarchy; another algorithm that needs the same formatting logic must duplicate it or inherit from an unrelated parent. - **Constructor coupling.** Subclasses must satisfy the base constructor, so base-class dependency changes ripple into every subclass's constructor. ## Failure mode 3 — comprehension and debugging cost Reading a three-level hierarchy means holding the composed method in your head across three files, resolving at each call whether *this* level or a lower one implements the step. Stack traces are dominated by base-class frames. "Yo-yo problem" is the classic name: you scroll up and down the hierarchy to answer one question. Deep hierarchies also make refactoring risky because tooling cannot tell you which override relied on which ordering nuance. ## Failure mode 4 — Liskov Substitution Principle violations The skeleton is written against an implicit contract: *this step returns non-null, does not throw, does not mutate the collection I am iterating, is cheap enough to call in a loop, is safe to call twice*. An override that breaks any of these keeps the type check happy but corrupts the algorithm. Common examples: throwing `UnsupportedOperationException` from a step the skeleton always calls; caching in a hook that the skeleton invokes per row; a hook that calls the template method again (re-entrancy). ## Failure mode 5 — testing friction Testing one overridden step means constructing a subclass instance and satisfying whatever the base class needs (constructor args, framework context, sometimes a container). You cannot test the step as a pure function. This pushes teams toward heavier integration tests and mocking of base-class internals — a smell in itself. ## Refactoring out: Replace Inheritance with Delegation Mechanical, incremental recipe: 1. **Name the varying steps.** For each abstract/hook method, define a small interface with one method (or accept a function type). 2. **Add fields to the base class** for those collaborators, with defaults that call the existing overridable methods — behaviour unchanged. 3. **Convert one subclass at a time**: move the override body into an implementation of the new interface; construct the (now concrete) base class with that collaborator instead of extending it. 4. **Delete the abstract methods** once no subclass remains; make the class `final`/closed. 5. Along the way, **thread state explicitly**: what the override read from protected fields must now arrive as parameters or in a small immutable context object. This is the real work — and the point where you discover how much the hierarchy was leaking. Other escape hatches: - **Higher-order functions.** In languages with first-class functions, `runInTransaction(block)` or `export(rows, format = ::csv)` gives you the skeleton *and* the swappability with no types at all. - **Decorator / middleware chain.** If what subclasses actually add is before/after behaviour, a pipeline of wrappers composes better than a hierarchy and supports multiple independent concerns. - **Keep the skeleton, inject the steps.** Often the best answer is not either/or: keep the final template method for its ordering and cleanup guarantees, but have each step delegate to an injected collaborator. ## When *not* to refactor If variation is closed and small, all subclasses live in one module you own, the steps genuinely need internal state, and the algorithm is stable — the hierarchy is fine and cheaper than the interface ceremony. Refactor when you observe the actual symptoms: growing combinations, third-party subclasses blocking base-class changes, steps you want to test or reuse alone, or runtime selection requirements.

  • Why is adding a new abstract step to a widely-used base class a breaking change, and what should you do instead?
    Every existing subclass fails to compile because it does not implement the new member. Instead ship it as a hook with a default that preserves current behaviour, document it, and (if you eventually need it mandatory) deprecate the default over a release cycle. Better still, evolve the algorithm behind an injected collaborator so no downstream type has to change.
  • How do you migrate a Template Method hierarchy to composition without a big-bang rewrite?
    Add collaborator fields to the base class whose defaults delegate to the existing overridable methods, so behaviour is unchanged. Convert subclasses one at a time to construct the base class with a collaborator instead of extending it. When the last subclass is gone, delete the abstract methods and seal the class.
  • What signals tell you a template method hierarchy has become the wrong tool?
    Subclass count multiplying across independent axes; wanting to swap a step at runtime; needing the step's logic in a second algorithm; being unable to change the base class because external teams subclass it; and tests that must build a subclass and framework context just to exercise a few lines of formatting logic.

Inheritance here is like building on a shared foundation: everything above depends on where the load-bearing walls are. Moving a wall (reordering a step) is invisible from the outside but can crack every floor above — including floors other people built and you never see.

saying these in an interview costs you the question

  • "Inheritance is free reuse" — it is white-box reuse; the subclass depends on internals the type system does not express.
  • Adding an abstract method to a released base class and calling it a minor version.
  • Deep multi-level hierarchies where each level overrides part of the skeleton — nearly impossible to reason about or change.
  • Overriding the template method itself to "just add logging", silently dropping cleanup.
  • Assuming composition is always better — for a closed, small, state-heavy variation set the extra interfaces are pure ceremony.
  • Refactoring to Strategy without addressing state: ending up with a strategy interface that takes ten parameters.

context