Template Method Pattern

The Template Method Pattern is a behavioral design pattern that defines the skeleton of an algorithm in a base class and lets subclasses override specific steps—without changing the overall structure.
Think of a recipe. The steps are always the same: prepare ingredients, cook, plate, serve. But the actual ingredients and cooking technique vary by dish. The recipe is the template; each dish is a subclass.
The Problem
Imagine you're building a data report generator. Different reports (CSV, PDF, HTML) follow the same overall process but differ in specific steps:
// Without Template Method - massive duplication
class CSVReportGenerator {
public void generateReport() {
// Step 1: fetch data
System.out.println("Fetching data from DB...");
// Step 2: parse data (CSV-specific)
System.out.println("Parsing data as CSV...");
// Step 3: format (CSV-specific)
System.out.println("Formatting as comma-separated values...");
// Step 4: export (CSV-specific)
System.out.println("Exporting as .csv file...");
// Step 5: notify
System.out.println("Sending notification...");
}
}
class PDFReportGenerator {
public void generateReport() {
// Step 1: fetch data (DUPLICATED!)
System.out.println("Fetching data from DB...");
// Step 2-4: PDF-specific (duplicates structure)
System.out.println("Parsing data as PDF...");
System.out.println("Formatting with layout engine...");
System.out.println("Exporting as .pdf file...");
// Step 5: notify (DUPLICATED!)
System.out.println("Sending notification...");
}
}
Problems:
Code duplication: The overall algorithm is copy-pasted across classes
Hard to maintain: Fixing a shared step requires updating every class
Inconsistency risk: One class might forget a step entirely
No enforced structure: Nothing stops a subclass from skipping critical steps
The Solution: Template Method Pattern
The Template Method Pattern puts the algorithm skeleton in a base class and delegates varying steps to subclasses.
The pattern separates:
What stays the same (template method in base class)
What varies (overridden steps in subclasses)
What's optional (hooks — overridable but with default behavior)
How It Solves Each Problem
| Problem | How Template Method Solves It |
|---|---|
| Code duplication | Shared steps live once in the base class template method. |
| Hard to maintain | Fix a shared step in one place—all subclasses benefit. |
| Inconsistency risk | The algorithm structure is enforced by the base class. |
| No enforced structure | templateMethod() is final—subclasses can't reorder steps. |
Key Components
AbstractClass: Defines the
templateMethod()and declares abstract/hook stepsConcreteClass: Implements the varying steps for a specific algorithm variant
Template Method: The
finalmethod that orchestrates the algorithmHook Methods: Optional overridable methods with default (often empty) implementations
How It Solves the Problem
Real-World Implementation
Example 1: Report Generator
// Abstract Class
abstract class ReportGenerator {
// Template Method - final so subclasses cannot change the order
public final void generateReport() {
fetchData();
parseData();
formatReport();
if (shouldExport()) {
exportReport();
}
sendNotification();
}
// Shared steps - implemented in base class
private void fetchData() {
System.out.println("Fetching data from database...");
}
private void sendNotification() {
System.out.println("Notification sent: report is ready.\n");
}
// Abstract steps - subclasses must implement
protected abstract void parseData();
protected abstract void formatReport();
protected abstract void exportReport();
// Hook - subclasses may override; default is true
protected boolean shouldExport() {
return true;
}
}
// Concrete Class - CSV
class CSVReportGenerator extends ReportGenerator {
@Override
protected void parseData() {
System.out.println("Parsing data as CSV rows...");
}
@Override
protected void formatReport() {
System.out.println("Formatting with comma separators...");
}
@Override
protected void exportReport() {
System.out.println("Exporting as report.csv");
}
}
// Concrete Class - PDF
class PDFReportGenerator extends ReportGenerator {
@Override
protected void parseData() {
System.out.println("Parsing data as PDF content blocks...");
}
@Override
protected void formatReport() {
System.out.println("Formatting with PDF layout engine...");
}
@Override
protected void exportReport() {
System.out.println("Exporting as report.pdf");
}
}
// Concrete Class - HTML (with hook override)
class HTMLReportGenerator extends ReportGenerator {
@Override
protected void parseData() {
System.out.println("Parsing data as HTML table rows...");
}
@Override
protected void formatReport() {
System.out.println("Formatting with HTML/CSS templates...");
}
@Override
protected void exportReport() {
System.out.println("Publishing to web server...");
}
@Override
protected boolean shouldExport() {
System.out.println("Checking server availability...");
return true;
}
}
// Client
public class ReportDemo {
public static void main(String[] args) {
System.out.println("=== CSV Report ===");
new CSVReportGenerator().generateReport();
System.out.println("=== PDF Report ===");
new PDFReportGenerator().generateReport();
System.out.println("=== HTML Report ===");
new HTMLReportGenerator().generateReport();
}
}
Example 2: Beverage Preparation (Classic GoF Example)
// Abstract Class
abstract class Beverage {
// Template Method
public final void prepare() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) {
addCondiments();
}
}
// Shared steps
private void boilWater() {
System.out.println("Boiling water...");
}
private void pourInCup() {
System.out.println("Pouring into cup...");
}
// Abstract steps
protected abstract void brew();
protected abstract void addCondiments();
// Hook - customer can skip condiments
protected boolean customerWantsCondiments() {
return true;
}
}
// Concrete Class - Tea
class Tea extends Beverage {
@Override
protected void brew() {
System.out.println("Steeping the tea bag...");
}
@Override
protected void addCondiments() {
System.out.println("Adding lemon...");
}
}
// Concrete Class - Coffee
class Coffee extends Beverage {
@Override
protected void brew() {
System.out.println("Dripping coffee through filter...");
}
@Override
protected void addCondiments() {
System.out.println("Adding milk and sugar...");
}
@Override
protected boolean customerWantsCondiments() {
System.out.println("Customer wants black coffee - skipping condiments.");
return false;
}
}
// Client
public class BeverageDemo {
public static void main(String[] args) {
System.out.println("=== Making Tea ===");
new Tea().prepare();
System.out.println("\n=== Making Coffee ===");
new Coffee().prepare();
}
}
Example 3: Data Migration Pipeline
// Abstract Class
abstract class DataMigrationPipeline {
// Template Method
public final void migrate() {
System.out.println("--- Starting Migration ---");
connect();
extractData();
transformData();
validateData();
loadData();
disconnect();
System.out.println("--- Migration Complete ---\n");
}
// Fixed shared steps
private void connect() {
System.out.println("Connecting to source and target systems...");
}
private void disconnect() {
System.out.println("Disconnecting from all systems.");
}
private void validateData() {
System.out.println("Validating transformed data...");
}
// Abstract steps
protected abstract void extractData();
protected abstract void transformData();
protected abstract void loadData();
}
// MySQL to PostgreSQL
class MySQLToPostgresPipeline extends DataMigrationPipeline {
@Override
protected void extractData() {
System.out.println("Extracting data from MySQL tables...");
}
@Override
protected void transformData() {
System.out.println("Transforming MySQL data types to PostgreSQL...");
}
@Override
protected void loadData() {
System.out.println("Loading data into PostgreSQL schema...");
}
}
// CSV to MongoDB
class CSVToMongoPipeline extends DataMigrationPipeline {
@Override
protected void extractData() {
System.out.println("Reading rows from CSV files...");
}
@Override
protected void transformData() {
System.out.println("Transforming CSV rows into JSON documents...");
}
@Override
protected void loadData() {
System.out.println("Inserting documents into MongoDB collections...");
}
}
// Client
public class MigrationDemo {
public static void main(String[] args) {
System.out.println("=== MySQL to PostgreSQL ===");
new MySQLToPostgresPipeline().migrate();
System.out.println("=== CSV to MongoDB ===");
new CSVToMongoPipeline().migrate();
}
}
Workflow Diagram
Real-World Use Cases
Report Generators: PDF, CSV, HTML reports with shared fetch/notify steps
Data Pipelines: ETL (Extract, Transform, Load) workflows
Web Frameworks: Spring's
JdbcTemplate,RestTemplateTest Frameworks: JUnit's
setUp()/tearDown()lifecycleBuild Tools: Maven/Gradle build lifecycle phases
Game Loops: Initialize → update → render → cleanup
Authentication Flows: Shared validation with custom login strategies
When to Use the Template Method Pattern
✅ Use When
Shared Algorithm Structure: Multiple classes follow the same steps
Controlled Extension: You want subclasses to customize only specific steps
Avoid Duplication: Common steps are duplicated across classes
Framework Hooks: You provide a framework with extension points
❌ Avoid When
Too Many Subclasses: If every step varies, consider Strategy Pattern instead
Deep Inheritance: Inheritance chains become hard to follow
Simple Algorithms: Overhead isn't justified for trivial cases
Benefits
Eliminate Duplication: Shared steps live in one place
Enforced Structure: Algorithm order guaranteed by
finaltemplate methodOpen/Closed Principle: Add new variants without modifying the base class
Hollywood Principle: "Don't call us, we'll call you"—base class controls flow
Drawbacks
Inheritance Required: Must subclass, which increases coupling
Liskov Risk: Subclasses must honor the base class contract
Debugging Complexity: Flow jumps between base and subclasses
Template Method vs Strategy Pattern
| Aspect | Template Method | Strategy |
|---|---|---|
| Mechanism | Inheritance | Composition |
| Variation | Compile-time | Runtime |
| Control | Base class | Client |
| Granularity | Step-level | Algorithm-level |
| Best For | Fixed structure, varying steps | Swappable full algorithms |
Best Practices
Make Template Method
final: Prevent subclasses from reordering stepsMinimize Abstract Methods: Only make steps abstract if they truly vary
Use Hooks Wisely: Provide hooks for optional customization
Name Methods Clearly: Step names should express intent, not implementation
Prefer Narrow Overrides: Subclasses should override as little as possible
Document the Template: Make the algorithm structure obvious with comments
Conclusion
The Template Method Pattern is one of the most elegant ways to enforce a consistent algorithm while allowing flexible customization. It's the backbone of countless frameworks and libraries—and once you see it, you'll spot it everywhere.
Remember: Define the skeleton, let subclasses fill in the flesh! 🦴
🎯 Key Takeaway
The Template Method Pattern is about defining the recipe once and letting subclasses choose the ingredients. When your algorithm structure is fixed but the steps vary—template it!
A junior dev asked, "Why is templateMethod() marked final?" The senior replied, "Because the last time I let someone override it, they skipped the validation step, deployed on Friday, and we spent the weekend undoing it." The junior slowly closed the PR. 😄
Happy Templating! 🦴✨






