Appearance
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
| Participant | Role |
|---|---|
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
XmlReportGeneratoris 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 Method | Strategy | |
|---|---|---|
| Mechanism | Inheritance | Composition |
| Binding | Compile-time | Runtime |
| Skeleton location | Base class | Context class |
| Variable parts | Abstract/hook methods | Injected strategy object |
| Runtime swap | Not possible | Supported via setter |
| Use when | Skeleton is fixed; steps vary | Algorithm 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
finaltemplate 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.
Related Concepts
- 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.