skip to content

When implementing Template Method, how do you decide which steps are abstract primitive operations versus hooks with defaults, and how do you stop subclasses from breaking the algorithm's skeleton?

level: middleimportance: should knowfreq 40%

answer

  1. no sensible default → abstract
  2. optional refinement → hook with default
  3. final template method, protected steps
  4. try/finally in the skeleton, not in overrides
  5. avoid "must call super"

basics

~20 s

Make a step abstract when the algorithm cannot run without it, so every subclass is forced to supply it. Make it a hook with a default when it is optional extra behaviour. Keep the main method non-overridable and the steps narrowly visible so no subclass can reorder or skip anything.

solid answer

~50 s

Rule of thumb: a step the algorithm is meaningless without becomes an abstract primitive operation — the compiler then forces every subtype to answer the question. A step that is optional refinement, or that has a correct default for most cases, becomes a hook: an empty method, a default value, or a boolean predicate the skeleton consults (`shouldRetry()`, `beforeCommit()`). Prefer few, small, well-named steps; each one is a promise you must keep supporting. To protect the skeleton: mark the template method final/sealed so ordering, validation and cleanup cannot be bypassed; give the steps the narrowest visibility that works (protected, not public) so callers cannot invoke them out of order; document each step's contract — when it is called, what it may assume, what it must not do (mutate skeleton-owned state, swallow exceptions, call back into the template method). Enforce invariants in the skeleton itself — validate step output, wrap steps in try/finally — rather than trusting overrides.

code

pseudocode · 20 lines
pseudocode
abstract class Job {
  // sealed skeleton: ordering + cleanup cannot be bypassed
  final run(ctx) {
    val input = load(ctx)                 // primitive: no default possible
    require(input != null, "load() returned null")   // skeleton validates the step
    beforeProcess(ctx)                    // behavioural hook, default no-op
    try {
      val out = process(input)            // primitive
      if (shouldPublish(out)) publish(out) // decision hook, default true
    } finally {
      cleanup(ctx)                        // invariant: always runs
    }
  }

  protected abstract load(ctx): Input
  protected abstract process(input): Output
  protected beforeProcess(ctx) { }              // hook
  protected shouldPublish(out) = true           // hook
  private cleanup(ctx) { ctx.close() }          // NOT overridable at all
}

go deeper

for a junior

Say abstract = must implement, hook = optional with a default, and that the main method should be final so the order can't change.

for a middle

Give the decision rule (is there a correct default?), name behavioural vs decision hooks, and mention protected visibility plus try/finally for cleanup.

for a senior

Discuss enforcing invariants mechanically rather than by documentation, the call-super anti-pattern, re-entrancy and threading contracts, and shared abstract test suites for implementers.

for a principal

Treat the step set as versioned API: adding an abstract step is a breaking change, so evolve via defaulted hooks; consider sealing the hierarchy or moving to a narrow injected interface when third parties extend it.

## Vocabulary first - **Template method** — the one concrete method holding the ordered skeleton. - **Primitive operation** — an abstract/unimplemented step the skeleton calls; every concrete subtype *must* supply it. - **Hook** — a step that already has an implementation (often empty, a *no-op*, or a default value); a subtype *may* override it. Hooks come in two flavours: **behavioural hooks** (`afterSave()` — inject extra work) and **decision hooks** (`shouldValidate(): Boolean` — influence the skeleton's control flow). ## Choosing abstract vs hook Ask three questions per step: 1. **Is there a correct default?** If no sensible default exists ("how do I format a row?"), make it abstract — a default would be a guess that silently produces wrong behaviour. If a good default exists ("log nothing extra"), make it a hook. 2. **Do I want the compiler to nag?** Abstract steps turn "you forgot to think about this" into a compile error. That is valuable for genuinely required decisions; it is a burden if 90% of subtypes would write the same body. In the latter case, ship a default and consider an intermediate abstract class for the common case. 3. **Is it required for correctness or is it a courtesy?** Correctness → abstract. Observation, metrics, logging, notification → hook. Additional guidance: - **Minimise the step count.** GoF explicitly notes the goal of *minimising the primitive operations a subclass must override*. Every step is public surface toward subtypes and cannot be removed later without breaking them. - **Name steps by intent, not position** — `formatRow`, not `step3` — and prefix lifecycle hooks consistently (`before…`/`after…`/`should…`/`on…`) so the calling order is guessable. - **Return values over side effects.** A step that *returns* the value the skeleton needs is easier to test and harder to misuse than one that mutates shared state. - **Avoid steps whose contract is "call super".** Requiring an override to invoke `super.step()` is a rule the compiler cannot enforce (the *call-super* anti-pattern). Instead, split it: keep the mandatory part in the final skeleton and give the subtype a separate empty hook. ## Protecting the skeleton The pattern's entire value is that the ordering and invariants are guaranteed. Techniques: 1. **Seal the template method.** `final` (Java/C#), non-`open` (Kotlin), non-`virtual` (C++), or the language's equivalent. If a subtype can override the skeleton, it can drop the cleanup and the guarantee is gone. 2. **Narrow the visibility of steps.** Steps are for subtypes, not callers: `protected` (Java/C#/Kotlin), or module-internal. A `public` step invites external code to call step 3 without steps 1–2. 3. **Put invariants in the skeleton, not in the contract text.** Wrap steps in `try/finally` so cleanup runs even if an override throws. Validate what a step returns (null/empty/out-of-range) before proceeding. Time or bound the step if it is untrusted. 4. **Restrict who can subtype** where the language allows — sealed hierarchies, or a package-private constructor — so the extension set stays known and evolvable. 5. **Document the contract per step**: exactly when it is invoked, what state is already established, what it must return, whether it may throw, whether it may be called concurrently or more than once, and what it must *not* touch. 6. **Guard against re-entrancy.** Say explicitly that a hook must not call the template method again; a re-entrant call can corrupt half-initialised state or loop forever. 7. **Test the contract, not just the happy path.** A shared abstract test base (itself a template method!) that every subtype's test class extends can assert cross-cutting rules: cleanup always runs, exceptions propagate as documented, the step is invoked exactly once. ## Failure modes this prevents - A subtype overrides the skeleton "just to add logging" and quietly loses the `finally` block, leaking connections. - An override forgets `super.init()`, so half the state is unset — a class of bug the compiler will never catch. - A hook throws an unexpected exception mid-loop, skipping the footer/cleanup, because the skeleton did not wrap it. - A step is public, so a caller invokes `format(row)` on a stream that was never opened. - A hook mutates a collection the skeleton is iterating, causing nondeterministic behaviour. ## Concurrency and lifecycle notes If instances are shared (a framework often holds one handler per route), overridden steps may run on many threads at once. State the threading contract: steps should be stateless or use only locals; per-invocation state belongs in a context object passed as a parameter, not in fields. This is one more reason to prefer steps that *return* results rather than accumulate into fields.

  • Why is requiring subclasses to call super.someStep() considered a smell?
    Because it is a contract the compiler cannot enforce — the first override that forgets it produces a subtle, silent bug. The fix is to keep the mandatory work in the sealed skeleton and expose a separate empty hook for the subtype's addition, so forgetting is impossible.
  • How do you keep the base class's contract honest across many subclasses you don't own?
    Enforce what you can mechanically: wrap steps in try/finally, validate returned values, seal the template method, keep steps protected. Then publish a shared abstract test suite that every implementer's test class extends, so the cross-cutting invariants (cleanup runs, exceptions propagate, step called once) are verified per implementation.
  • A step needs data that only exists mid-algorithm. How should you pass it?
    As explicit parameters or a small immutable context object handed to the step, not via mutable fields on the base class. That keeps the step testable in isolation, makes the dependency visible, and avoids shared-state bugs when instances are reused across threads.

saying these in an interview costs you the question

  • Making every step abstract, forcing boilerplate overrides that all say the same thing.
  • Leaving the template method overridable, so a subclass can silently drop cleanup or reorder steps.
  • Exposing steps as public methods, letting external callers invoke them out of sequence.
  • Relying on documentation ("remember to call super") for invariants the skeleton could enforce with try/finally.
  • Storing per-invocation state in base-class fields, which breaks when one instance handles concurrent calls.
  • Adding a new abstract step to a released base class — it breaks every existing subclass at compile time.

context