Skip to content

Template Method Pattern

Introduction

The Template Method pattern defines the skeleton of an algorithm in a base class and defers specific steps to subclasses, without changing the algorithm's overall structure. It is the object-oriented embodiment of the Hollywood Principle — "don't call us, we'll call you" — because the superclass drives execution and calls down into subclass-provided steps, inverting the usual control flow. The pattern is pervasive in framework design: Spring's JdbcTemplate, Java's HttpServlet, and AWS Lambda handler base classes all use it to provide a fixed processing pipeline while letting application code fill in the domain-specific steps.

Intent

Define the skeleton of an algorithm in a base class operation, deferring some steps to subclasses so that subclasses can redefine specific steps without changing the algorithm's structure.

Structure

Participants

ParticipantRole
AbstractClass (ReportGenerator)Defines the template method (generate()) and declares abstract steps and optional hooks.
ConcreteClass (CsvReportGenerator, PdfReportGenerator, HtmlReportGenerator)Implements each abstract step. May optionally override hooks; must not override the template method itself.
Template Method (generate())The fixed algorithm skeleton — declared final to prevent subclasses from breaking the invariant.
Abstract Steps (fetchData(), transformData(), renderOutput())Mandatory overrides — each subclass must implement them.
Hooks (getReportTitle(), onComplete())Optional overrides with default behavior — subclasses may customize them, but are not required to.

Anti-Pattern — Duplicated Pipeline

Without Template Method, each report class independently implements the entire fetch-transform-render pipeline. The pipeline structure is duplicated in every class; a bug fix or cross-cutting concern (logging, timing) must be applied to every copy.

java
// ANTI-PATTERN: pipeline logic duplicated across report classes
public class CsvReportGenerator {

    public void generate() {
        // Step 1: fetch — duplicated
        System.out.println("Fetching data for CSV report...");
        List<Row> rows = dataSource.queryAll();

        // Step 2: transform — duplicated structure, different logic
        System.out.println("Transforming data for CSV...");
        List<String> csvRows = rows.stream()
            .map(row -> String.join(",", row.values()))
            .collect(Collectors.toList());

        // Step 3: render — duplicated structure, different output
        System.out.println("Rendering CSV...");
        String output = String.join("\n", csvRows);
        fileSystem.write("report.csv", output);

        System.out.println("CSV report complete.");
    }
}

public class PdfReportGenerator {

    public void generate() {
        // Step 1: fetch — identical boilerplate
        System.out.println("Fetching data for PDF report...");
        List<Row> rows = dataSource.queryAll();

        // Step 2: transform — identical boilerplate
        System.out.println("Transforming data for PDF...");
        Document doc = pdfEngine.createDocument("Report");
        rows.forEach(row -> pdfEngine.addRow(doc, row));

        // Step 3: render — identical boilerplate
        System.out.println("Rendering PDF...");
        byte[] bytes = pdfEngine.render(doc);
        fileSystem.writeBytes("report.pdf", bytes);

        System.out.println("PDF report complete.");
    }
}

Problems:

  • Any cross-cutting change — adding timing metrics, pre/post logging, error handling — must be replicated in every generator class.
  • The pipeline contract (fetch → transform → render) is implicit; nothing enforces it.
  • If a new XmlReportGenerator is added, the developer must remember to follow the same three-step structure by convention.

Correct Implementation

Abstract Base Class with Template Method

java
import java.time.Duration;
import java.time.Instant;

public abstract class ReportGenerator {

    // Template method — final prevents subclasses from breaking the pipeline invariant
    public final void generate() {
        Instant start = Instant.now();
        System.out.printf("[%s] Starting report generation%n", getReportTitle());

        ReportData rawData = fetchData();
        ReportData processedData = transformData(rawData);
        renderOutput(processedData);

        Duration elapsed = Duration.between(start, Instant.now());
        System.out.printf("[%s] Report complete in %dms%n",
            getReportTitle(), elapsed.toMillis());

        onComplete(); // hook — called after every successful generation
    }

    // Abstract steps — mandatory overrides; subclasses define the "what"
    protected abstract ReportData fetchData();
    protected abstract ReportData transformData(ReportData rawData);
    protected abstract void renderOutput(ReportData data);

    // Hook — optional override; provides a default but subclasses may specialize
    protected String getReportTitle() {
        return getClass().getSimpleName();
    }

    // Hook — optional post-completion callback; default is no-op
    protected void onComplete() {
        // default: do nothing
    }
}

Data Transfer Object

java
import java.util.List;
import java.util.Map;

public record ReportData(String title, List<Map<String, Object>> rows) {}

Concrete Classes

java
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class CsvReportGenerator extends ReportGenerator {

    private final DataSource dataSource;
    private final FileSystem fileSystem;

    public CsvReportGenerator(DataSource dataSource, FileSystem fileSystem) {
        this.dataSource = dataSource;
        this.fileSystem = fileSystem;
    }

    @Override
    protected ReportData fetchData() {
        System.out.println("  [CSV] Fetching rows from data source");
        List<Map<String, Object>> rows = dataSource.queryAll();
        return new ReportData("Sales Report", rows);
    }

    @Override
    protected ReportData transformData(ReportData rawData) {
        System.out.println("  [CSV] Sorting and filtering rows");
        List<Map<String, Object>> sorted = rawData.rows().stream()
            .sorted((a, b) -> String.valueOf(a.get("date"))
                                   .compareTo(String.valueOf(b.get("date"))))
            .collect(Collectors.toList());
        return new ReportData(rawData.title(), sorted);
    }

    @Override
    protected void renderOutput(ReportData data) {
        System.out.println("  [CSV] Writing CSV file");
        StringBuilder sb = new StringBuilder();
        if (!data.rows().isEmpty()) {
            sb.append(String.join(",", data.rows().get(0).keySet())).append("\n");
            data.rows().forEach(row ->
                sb.append(String.join(",", row.values().stream()
                    .map(String::valueOf).collect(Collectors.toList())))
                  .append("\n")
            );
        }
        fileSystem.write("report.csv", sb.toString());
    }

    @Override
    protected String getReportTitle() {
        return "CSV Sales Report"; // overrides the hook
    }
}
java
public class PdfReportGenerator extends ReportGenerator {

    private final DataSource dataSource;
    private final PdfEngine pdfEngine;
    private final S3Client s3Client;
    private final String bucketName;

    public PdfReportGenerator(DataSource dataSource, PdfEngine pdfEngine,
                               S3Client s3Client, String bucketName) {
        this.dataSource = dataSource;
        this.pdfEngine = pdfEngine;
        this.s3Client = s3Client;
        this.bucketName = bucketName;
    }

    @Override
    protected ReportData fetchData() {
        System.out.println("  [PDF] Fetching aggregated metrics");
        List<Map<String, Object>> rows = dataSource.queryAggregated();
        return new ReportData("Executive Summary", rows);
    }

    @Override
    protected ReportData transformData(ReportData rawData) {
        System.out.println("  [PDF] Computing subtotals and highlights");
        // Apply business-specific transformations — totals, highlights, etc.
        return rawData; // simplified
    }

    @Override
    protected void renderOutput(ReportData data) {
        System.out.println("  [PDF] Generating PDF and uploading to S3");
        byte[] pdfBytes = pdfEngine.render(data);
        String key = "reports/" + data.title().replace(" ", "_") + ".pdf";
        s3Client.putObject(PutObjectRequest.builder().bucket(bucketName).key(key).build(),
                           RequestBody.fromBytes(pdfBytes));
        System.out.println("  [PDF] Uploaded to s3://" + bucketName + "/" + key);
    }

    @Override
    protected void onComplete() {
        // Hook override: notify stakeholders after upload completes
        System.out.println("  [PDF] Sending notification to stakeholders");
    }
}

Hooks vs. Abstract Steps

Execution Flow

Real-World Examples

java.util.AbstractList

AbstractList provides iterator(), listIterator(), indexOf(), subList(), and many other operations as a template built on two abstract steps: get(index) and size(). Subclasses implement those two methods; the full List contract is provided for free.

java
// Minimal read-only list backed by an array — only two methods required
public class ArrayBackedList<T> extends AbstractList<T> {

    private final T[] data;

    @SuppressWarnings("unchecked")
    public ArrayBackedList(T... data) {
        this.data = data;
    }

    @Override
    public T get(int index) { return data[index]; }    // abstract step

    @Override
    public int size() { return data.length; }          // abstract step

    // All other List operations — contains(), iterator(), subList(), etc.
    // — are inherited from AbstractList without any extra code.
}

javax.servlet.HttpServlet

HttpServlet.service() is the template method. It reads the HTTP method and dispatches to doGet(), doPost(), doPut(), or doDelete(). Application code overrides only the verbs it handles; the dispatch skeleton is never touched.

java
@WebServlet("/orders")
public class OrderServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String orderId = req.getParameter("id");
        // fetch and render order — GET logic only
        resp.getWriter().write("{\"orderId\": \"" + orderId + "\"}");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        // parse body and create order — POST logic only
        resp.setStatus(HttpServletResponse.SC_CREATED);
    }

    // service() is the template method — never overridden
    // It calls doGet() or doPost() depending on the request method
}

Spring JdbcTemplate

JdbcTemplate encodes the acquire-connection → execute-SQL → handle-results → release-connection pipeline. Application code provides only the SQL and a RowMapper callback — a Strategy injected into the template step.

java
@Repository
public class OrderRepository {

    private final JdbcTemplate jdbc;

    public OrderRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    public List<Order> findByCustomer(String customerId) {
        // JdbcTemplate owns the pipeline; we supply the variable steps as lambdas
        return jdbc.query(
            "SELECT * FROM orders WHERE customer_id = ?",
            (rs, rowNum) -> new Order(            // RowMapper — template step override
                rs.getString("id"),
                rs.getString("customer_id"),
                rs.getBigDecimal("amount"),
                rs.getString("status")
            ),
            customerId
        );
    }
}

AWS Lambda Handler Base Class

AWS Lambda Java base classes (e.g., RequestHandler<I, O>) define the invocation pipeline — deserialize input, execute handler, serialize output, handle errors. Application code provides handleRequest(), the single abstract step.

java
public class OrderCreatedHandler
        implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

    @Override
    public APIGatewayProxyResponseEvent handleRequest(
            APIGatewayProxyRequestEvent input, Context context) {
        // Only the domain logic lives here; the Lambda runtime owns the pipeline
        String body = input.getBody();
        Order order = parseOrder(body);
        orderService.create(order);
        return new APIGatewayProxyResponseEvent()
            .withStatusCode(201)
            .withBody("{\"id\": \"" + order.getId() + "\"}");
    }
}

A common pattern is to extend this further with an application-defined abstract base that adds cross-cutting concerns:

java
// Application-defined base — adds auth, validation, and error handling
public abstract class SecureOrderHandler
        implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

    @Override
    public final APIGatewayProxyResponseEvent handleRequest(
            APIGatewayProxyRequestEvent input, Context context) {
        try {
            authenticate(input);        // always runs
            validate(input);            // always runs
            return execute(input, context);  // abstract step — subclass provides domain logic
        } catch (AuthException ex) {
            return errorResponse(401, ex.getMessage());
        } catch (ValidationException ex) {
            return errorResponse(400, ex.getMessage());
        }
    }

    protected abstract APIGatewayProxyResponseEvent execute(
        APIGatewayProxyRequestEvent input, Context context);

    private void authenticate(APIGatewayProxyRequestEvent input) { /* JWT validation */ }
    private void validate(APIGatewayProxyRequestEvent input) { /* schema validation */ }
    private APIGatewayProxyResponseEvent errorResponse(int status, String message) {
        return new APIGatewayProxyResponseEvent()
            .withStatusCode(status)
            .withBody("{\"error\": \"" + message + "\"}");
    }
}

Template Method vs. Strategy

These two patterns solve similar problems — separating the stable parts of an algorithm from the variable parts — but they use different mechanisms with different trade-off profiles.

Template MethodStrategy
MechanismInheritanceComposition
BindingCompile-timeRuntime
Skeleton locationBase classContext class
Variable partsAbstract/hook methodsInjected strategy object
Runtime swapNot possibleSupported via setter
Use whenSkeleton is fixed; steps varyAlgorithm itself must be swappable

Prefer Template Method when the algorithm's skeleton is stable and the only variation is in specific steps. Prefer Strategy when the whole algorithm — or its selection — must vary at runtime. The two are sometimes combined: a template method skeleton calls abstract steps that delegate to injected strategies.

The Hollywood Principle

The Template Method pattern is the clearest illustration of the Hollywood Principle in object-oriented design: the superclass calls the subclass, not the other way around. Subclasses do not control the flow — they implement the steps the superclass requests.

This inversion of control is what frameworks exploit. Spring, the Servlet container, and the Lambda runtime all call into application code at specific, well-defined points — they own the skeleton; applications fill in the steps.

When to Use

  • Multiple classes share the same algorithm skeleton but differ in one or more steps.
  • You want to avoid code duplication in the skeleton while allowing subclasses to vary specific behaviors.
  • You are building a framework or library and want to provide a fixed pipeline that application code plugs into.
  • Cross-cutting concerns (logging, metrics, error handling) must be guaranteed to run around all implementations.

Consequences

Benefits

  • Eliminates duplicate pipeline code: the skeleton lives in exactly one place.
  • Cross-cutting concerns added to the template method automatically apply to all subclasses.
  • The final template method enforces the algorithm contract — subclasses cannot accidentally break the pipeline order.
  • Hooks let subclasses extend behavior at defined points without forcing all subclasses to implement every extension.

Trade-offs

  • Inheritance couples subclasses to the base class. A change to the base class signature affects every subclass.
  • Deep inheritance hierarchies become difficult to follow — the "yo-yo problem" of tracing execution up and down the class hierarchy.
  • The algorithm is fixed at compile time; Template Method cannot accommodate runtime algorithm selection (use Strategy for that).
  • Hooks with default behavior can be silently ignored by subclasses, leading to subtle bugs if the developer forgets they exist.
  • Strategy Pattern: The composition-based alternative. Strategy swaps the full algorithm at runtime; Template Method fixes the skeleton at compile time. Both achieve the same goal of separating stable structure from variable behavior, but through different mechanisms.
  • Observer Pattern: Template methods often contain hook points that fire events; those events can be delivered to observers, combining both patterns.
  • Open/Closed Principle: Template Method is closed for modification (the skeleton in the final method), open for extension (new subclasses add new step implementations).
  • Clean Architecture: Framework layers frequently use Template Method to define processing pipelines (request handling, use-case orchestration) while delegating domain-specific logic to the application layer.
  • Saga Pattern: Orchestration-based sagas define a fixed sequence of compensatable steps — a conceptual template — while individual step implementations vary per saga type.