Engineering Atlas › Low-Level Design

Template Method pattern

14 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

The problem

A data platform runs nightly batch jobs: import invoices, recompute loyalty points, expire coupons. Each job class was written by a different engineer, and each has its own run(): open a database connection, loop over items, handle errors, close the connection, write a summary. They drifted. The coupon job closes its connection only on success, so a failure leaks it. The invoice job swallows every error. The loyalty job counts skipped items as processed. Every new job copies one of the three and inherits its bugs.

The jobs differ in two or three places: how an item is processed, which items are skipped, and whether one bad item stops the run. Everything else, the order of steps, the error accounting, the guarantee that cleanup happens, should be written once.

The skeleton and its steps

Template Method moves the outline into a base class method and turns each varying part into a named method call:

class BatchJob:
    def run(self, items):            # the template method
        self.setup()
        try:
            for item in items:
                if self.should_skip(item):    # hook, default False
                    continue
                self.process(item)            # abstract step
        finally:
            self.teardown()                   # hook, default no-op

    def should_skip(self, item): return False
    def setup(self): pass
    def teardown(self): pass

A coupon job now overrides process and perhaps should_skip, and nothing else. It cannot leak the connection, because it never writes the finally. Mark the template method so subclasses do not override it: final in Java, a non-virtual function in C++, typing.final in Python (enforced by type checkers, not at runtime).

Hooks versus abstract steps

Abstract steps are the parts with no sensible default; leaving one out should fail loudly, when the subclass is instantiated (abc.abstractmethod) or at compile time. Hooks are extension points with a safe default, so most subclasses ignore them. Keep the number of steps small. A skeleton with twelve hooks is a framework nobody can reason about, because every subclass is a different program. Name hooks for when they run (before_item, on_error) and document whether their return value changes control flow.

Where you already use it

The composition version

Template Method and Strategy solve the same problem with different tools (see Strategy). Template Method varies steps by subclassing; Strategy passes the varying part in as an object. Spring's JdbcTemplate is named after the pattern but uses callbacks: the template owns connection, statement and exception translation, and you pass a RowMapper. Callbacks let one class combine steps from different sources and change them at runtime; inheritance gives one fixed combination per subclass. The rule of thumb: inheritance when a framework owns the lifecycle and steps share state on self; composition everywhere else.

Go: embedding is not inheritance

Engineers from Java often try this in Go: a BaseJob struct with a Run method that calls j.Process(item), embedded in CouponJob, which defines its own Process. It compiles, and Run calls BaseJob.Process, not CouponJob.Process, because a promoted method's receiver is the embedded value and Go has no virtual dispatch on structs. The idiomatic version is a function Run(job Steps, items) that takes an interface for the required steps. Optional hooks become small extra interfaces checked with a type assertion, the same trick io.Copy uses when it checks for WriterTo and ReaderFrom.

Reviewing agent-written code

Asked to "add a job that expires coupons", an agent will usually find the nearest job, copy its whole class, and change the body of the loop. The diff looks small and the bug surface doubles. When a base class exists, agents also override the template method itself to "add one thing", which silently drops the invariants. Point the agent at the skeleton, list the steps it may override, and in review check three things: the template method is not overridden; subclasses do not call each other's steps out of order; and the skeleton's guarantees (cleanup, accounting, error policy) are covered by tests written against the base class with a fake subclass.

The same shape elsewhere

Template Method is multi-step consistency inside one process: a fixed sequence where some steps must always run once others have. The same shape governs a database transaction (begin, work, then exactly one of commit or rollback), a CI pipeline (checkout, build, test, then upload artifacts and report status even on failure), and Kubernetes pod lifecycle hooks (postStart and preStop around a container the platform runs). In every case the platform owns the order and the user supplies the middle.

Down the stack

Layer 1The contract

The template method promises an order and a set of guarantees: which steps run, in what sequence, which run on failure, and what happens to errors. For the batch job: setup runs first; if it fails, nothing else runs and the run is reported as aborted; once it succeeds, teardown runs exactly once whatever happens; each item is skipped, processed or failed, never two of these. Subclasses promise that each step does only its own job and does not call the template method again (re-entrancy is usually unsupported). Hooks promise that their defaults preserve the base behaviour, so a subclass that overrides nothing still behaves correctly.

Layer 2The mechanism

In class-based languages the skeleton calls steps through virtual dispatch. In C++ and Java, each object carries a pointer to its class's virtual method table, and this->process(item) loads the function pointer from a fixed slot. Herb Sutter's non-virtual interface idiom makes this explicit: the public method is non-virtual and owns the invariants, the customisation points are private virtual functions. Python looks steps up through the class's method resolution order on each call (type(self).__mro__), cached per type with a version tag that is invalidated when a class is modified. Java's AbstractList shows the cost of a poor primitive choice: its iterator calls get(i) per element, which is O(1) for an array but O(n) for a linked structure, so AbstractSequentialList exists to flip the primitive to a listIterator.

Go's mechanism is the interface: sort.Sort(data) calls data.Less(i, j) and data.Swap(i, j) through the itab, about n log n times each. The generic slices.Sort takes the same skeleton and lets the compiler specialise comparisons for the element type, which removes those interface calls; since Go 1.22, sort.Ints and friends call into slices.Sort for that reason.

Layer 3The machine

A virtual call costs a dependent load of the vtable pointer, a load of the function pointer, and an indirect branch: around 1 to 3 ns when predicted, and 10 to 20 ns when the predictor misses. The bigger cost is lost inlining. If less is a two-instruction integer compare, calling it indirectly 20 million times while sorting a million items adds tens of milliseconds and blocks vectorisation. JITs recover this: HotSpot uses class hierarchy analysis and call-site profiles to inline a step when only one subclass is loaded or seen, guarded by a cheap type check, and deoptimises if a new subclass appears. C++ compilers devirtualise when the class or method is marked final. In CPython, a method call on self costs roughly 50 to 100 ns, so a step called per item in a million-item batch adds a fraction of a second, which matters only when the step itself is trivial.

The skeleton is also where resource lifetime is enforced at the machine level. The finally that calls teardown is what closes the file descriptor or returns the pooled connection. A job that leaks one connection per failed run exhausts a pool of 20 after 20 failures, a common way that "the batch is down" turns into "the API is down" when both share a database.

Why it's built this way

  1. Many tasks share an outline and differ in a few steps, and copies of the outline drift.
  2. The outline holds invariants (cleanup, accounting, ordering) that must hold for every variant, so it should exist once and not be overridable.
  3. The varying steps must be called by the outline, so the outline needs an indirect call: a virtual method, an interface, or a callback.
  4. Inheritance makes those calls cheap to write when the framework owns the object's lifecycle, which is why frameworks use it.
  5. Inheritance also couples subclass to base and fixes the combination at class definition, so outside frameworks the same idea is expressed with composition.

Diagram

sequenceDiagram
  participant C as Caller
  participant T as BatchJob.run
  participant S as Subclass steps
  C->>T: run(items)
  T->>S: setup()
  loop each item
    T->>S: should_skip(item)
    T->>S: process(item)
    T--)S: on_error(item, err) if it failed
  end
  T->>S: teardown()
  T-->>C: Report

Worked example

A payments company had 31 nightly jobs, each with its own copy of the run loop. An outage review found that 9 of them closed their database connection only on success. On the night a schema migration made 7 of those jobs fail, they leaked 7 connections each on retry (5 retries, 35 connections), exhausting the 40-connection pool the jobs shared with the merchant dashboard API, which returned errors for 26 minutes.

The fix introduced a BatchJob base class whose run is final and owns setup, teardown in a finally, error policy and a structured report (processed, skipped, failed, aborted). The 31 jobs were migrated over three weeks: on average each lost 38 lines and kept 12, the code that actually differed. A test suite for the base class uses a recording subclass to assert the order of calls, including "setup fails, teardown not called" and "process raises, teardown still called". Connection leaks from batch jobs have not recurred, and the dashboard and batch pools were split as defence in depth.

The overhead of the extra method calls was measured on the largest job, 4.2 million items: about 0.4 s of a 38-minute run, under 0.02%.

Trade-offs

OptionStrengthsWeaknessesChoose when
Copy the loop per jobNo abstraction to learnInvariants drift; bugs multiplyOne job, or truly different outlines
Template Method (base class)Invariants written once; steps share state on selfInheritance coupling; one combination per subclassA framework owns the lifecycle
Strategy or callbacksSteps mix and change at runtime; easy to test aloneMore wiring; steps cannot share self stateApplication code; several independent variation points
Function over an interface (Go)No inheritance; optional hooks by type assertionHooks less discoverableGo, or any language favouring composition
Context manager or deferLightweight lifecycle for one resourceCovers setup and cleanup, not a multi-step outlineOnly the "always clean up" part is needed

Pitfalls in production

Leadership lens

Check yourself

  1. Why does unittest skip tearDown when setUp raises, but still run cleanups registered with addCleanup?

    Show answer

    tearDown may assume everything setUp created exists, so running it after a partial setup can fail confusingly; cleanups are registered as each resource is created, so they release exactly what was acquired.

  2. In Go, a BaseJob with method Run calling b.Process() is embedded in CouponJob, which defines Process. Which Process runs, and how do you fix the design?

    Show answer

    BaseJob.Process, because promoted methods have the embedded value as receiver and structs have no virtual dispatch. Write Run(job Steps, items) as a function over an interface that CouponJob implements.

  3. When would you replace a template method with Strategy?

    Show answer

    When variations are independent and should be mixed (for example skip policy and error policy chosen separately), changed at runtime, or tested alone; inheritance gives one fixed combination per subclass.

  4. Why can a JIT make a template method's step calls nearly free while CPython cannot?

    Show answer

    The JIT sees which subclass actually reaches a call site and inlines that method behind a type guard; CPython looks up and calls the method dynamically each time.

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →