Mediumbehavioral18 min read

Template Method Pattern

1. What is Template Method Pattern?

Definition

The Template Method pattern is a GOF Behavioral Pattern. Template means blueprint or preset format. It defines the skeleton of an algorithm in a method (the template method), deferring some steps to subclasses. Subclasses can redefine certain steps of the algorithm without changing the algorithm's overall structure and step order.

One line: Fix the algorithm order in the superclass, let subclasses fill in the details.

The template method DP is a superclass/framework that defines the skeleton of operations. Common steps are implemented in the superclass. Specific steps are declared as abstract methods and implemented in subclasses. The order of steps is locked in a final template method so no subclass can change it.

Real-world analogy

Fresher Recruitment Drive at CMR College: The recruitment process for Java, DotNet, and PHP freshers follows the same algorithm with 5 steps in a fixed order:

  1. Aptitude Test (common for all)
  2. Group Discussion (common for all)
  3. Technical Written Test (differs per technology: Java / DotNet / PHP)
  4. Technical Interview (differs per technology)
  5. HR Test (common for all)

The common steps are identical regardless of technology. Only steps 3 and 4 vary. The order never changes. This is a template method.

2. The Problem

Problem

When each subclass independently implements the full algorithm:

  • Common steps (Aptitude, GD, HR) are duplicated in every class
  • The order of steps is not enforced. Each client must remember the correct sequence
  • The full algorithm burden is placed on the client. A single client mistake breaks the process
  • All firms, services, and implementations repeat the same sequence of operations that could be completed by taking support of a template

Problem code without template method

JavaTester.java (all 5 steps duplicated)

package com.tt.recruitment;

public class JavaTester {
    public boolean conductAptitudeTest() {
        System.out.println("JavaTester :: conductAptitudeTest()");
        return true;
    }
    public boolean conductGroupDiscussion() {
        System.out.println("JavaTester :: conductGroupDiscussion()");
        return true;
    }
    public boolean conductTechnicalWrittenTest() {
        System.out.println("JavaTester :: conductTechnicalWrittenTest() - Java");
        return true;
    }
    public boolean conductTechnicalInterview() {
        System.out.println("JavaTester :: conductTechnicalInterview() - Java");
        return true;
    }
    public boolean conductHRTest() {
        System.out.println("JavaTester :: conductHRTest()");
        return true;
    }
}

DotNetTester.java (same 5 steps duplicated again)

package com.tt.recruitment;

public class DotNetTester {
    public boolean conductAptitudeTest() {
        System.out.println("DotNetTester :: conductAptitudeTest()");
        return true;
    }
    public boolean conductGroupDiscussion() {
        System.out.println("DotNetTester :: conductGroupDiscussion()");
        return true;
    }
    public boolean conductTechnicalWrittenTest() {
        System.out.println("DotNetTester :: conductTechnicalWrittenTest() - DotNet");
        return true;
    }
    public boolean conductTechnicalInterview() {
        System.out.println("DotNetTester :: conductTechnicalInterview() - DotNet");
        return true;
    }
    public boolean conductHRTest() {
        System.out.println("DotNetTester :: conductHRTest()");
        return true;
    }
}

Problem client code (client must know all steps and their order)

// Client1.java
package com.tt.test;

import com.tt.recruitment.JavaTester;

public class Client1 {
    public static void main(String[] args) {
        JavaTester tester = new JavaTester();

        // Client must remember the FULL algorithm AND correct order
        boolean flag = tester.conductAptitudeTest();
        if (flag) flag = tester.conductGroupDiscussion();
        if (flag) flag = tester.conductTechnicalWrittenTest();
        if (flag) flag = tester.conductTechnicalInterview();
        if (flag) flag = tester.conductHRTest();

        System.out.println(flag ? "Candidate Hired" : "Candidate Not Hired");
    }
}

// Client2.java - same algorithm + order duplicated AGAIN
package com.tt.test;

import com.tt.recruitment.DotNetTester;

public class Client2 {
    public static void main(String[] args) {
        DotNetTester tester = new DotNetTester();

        boolean flag = tester.conductAptitudeTest();
        if (flag) flag = tester.conductGroupDiscussion();
        if (flag) flag = tester.conductTechnicalWrittenTest();
        if (flag) flag = tester.conductTechnicalInterview();
        if (flag) flag = tester.conductHRTest();

        System.out.println(flag ? "Candidate Hired" : "Candidate Not Hired");
    }
}
Problem: each class is separate, no enforced order, code duplication JavaTester DotNetTester PhpTester +-----------+ +-----------+ +-----------+ | aptitude | | aptitude | | aptitude | | gd | SAME CODE | gd | SAME CODE | gd | | written | DUPLICATED | written | DUPLICATED | written | | interview | | interview | | interview | | hr | | hr | | hr | +-----------+ +-----------+ +-----------+ Client must call all 5 methods in exact order. If client makes a mistake in order --> recruitment process is wrong.

3. The Solution: Template Method

Solution

Create an abstract superclass with:

  1. Common steps as concrete methods (implemented once)
  2. Specific steps as abstract methods (subclasses implement)
  3. Template method marked final that calls all steps in the fixed order

The client calls only the template method. One call runs the entire algorithm.

HireTester (abstract superclass) +-- conductAptitudeTest() [concrete - common, implemented here] +-- conductGroupDiscussion() [concrete - common, implemented here] +-- conductTechnicalWrittenTest() [ABSTRACT - specific, subclass implements] +-- conductTechnicalInterview() [ABSTRACT - specific, subclass implements] +-- conductHRTest() [concrete - common, implemented here] +-- hire() [FINAL TEMPLATE METHOD - fixed order] | +---- HireJavaTester (implements written + interview for Java) +---- HireDotNetTester (implements written + interview for DotNet) +---- HirePhpTester (implements written + interview for PHP)

HireTester.java (abstract superclass with template method)

package com.tt.recruitment;

public abstract class HireTester {

    // COMMON steps - implemented once, shared by all subclasses
    public boolean conductAptitudeTest() {
        System.out.println("HireTester :: conductAptitudeTest()");
        return true;
    }

    public boolean conductGroupDiscussion() {
        System.out.println("HireTester :: conductGroupDiscussion()");
        return true;
    }

    public boolean conductHRTest() {
        System.out.println("HireTester :: conductHRTest()");
        return true;
    }

    // SPECIFIC steps - abstract, each technology implements its own
    public abstract boolean conductTechnicalWrittenTest();
    public abstract boolean conductTechnicalInterview();

    // TEMPLATE METHOD - final: order cannot be changed by subclass or client
    public final boolean hire() {
        boolean flag = conductAptitudeTest();
        if (flag) flag = conductGroupDiscussion();
        if (flag) flag = conductTechnicalWrittenTest();
        if (flag) flag = conductTechnicalInterview();
        if (flag) flag = conductHRTest();
        return flag;
    }
}

Concrete subclasses

// HireJavaTester.java
package com.tt.recruitment;

public class HireJavaTester extends HireTester {

    @Override
    public boolean conductTechnicalWrittenTest() {
        System.out.println("HireJavaTester :: conductTechnicalWrittenTest() - Java");
        return true;
    }

    @Override
    public boolean conductTechnicalInterview() {
        System.out.println("HireJavaTester :: conductTechnicalInterview() - Java");
        return true;
    }
}

// HireDotNetTester.java
package com.tt.recruitment;

public class HireDotNetTester extends HireTester {

    @Override
    public boolean conductTechnicalWrittenTest() {
        System.out.println("HireDotNetTester :: conductTechnicalWrittenTest() - DotNet");
        return true;
    }

    @Override
    public boolean conductTechnicalInterview() {
        System.out.println("HireDotNetTester :: conductTechnicalInterview() - DotNet");
        return true;
    }
}

// HirePhpTester.java
package com.tt.recruitment;

public class HirePhpTester extends HireTester {

    @Override
    public boolean conductTechnicalWrittenTest() {
        System.out.println("HirePhpTester :: conductTechnicalWrittenTest() - PHP");
        return true;
    }

    @Override
    public boolean conductTechnicalInterview() {
        System.out.println("HirePhpTester :: conductTechnicalInterview() - PHP");
        return true;
    }
}

Factory to create testers

package com.tt.factory;

import com.tt.recruitment.*;

public class TesterFactory {
    public static HireTester getHireTester(String type) {
        if (type == null) {
            throw new IllegalArgumentException("Tester type cannot be null");
        }
        if (type.equalsIgnoreCase("java")) return new HireJavaTester();
        if (type.equalsIgnoreCase("dotnet")) return new HireDotNetTester();
        if (type.equalsIgnoreCase("php")) return new HirePhpTester();
        throw new IllegalArgumentException("Invalid tester type: " + type);
    }
}

Client code (single call runs entire algorithm)

// CmrApp.java
package com.tt.test;

import com.tt.recruitment.HireTester;
import com.tt.factory.TesterFactory;

public class CmrApp {
    public static void main(String[] args) {
        // Get the proper tester from factory
        HireTester tester = TesterFactory.getHireTester("java");

        // SINGLE call runs the whole fixed algorithm
        boolean result = tester.hire();
        System.out.println(result ? "Candidate Hired" : "Candidate Not Hired");
    }
}
HireTester :: conductAptitudeTest() HireTester :: conductGroupDiscussion() HireJavaTester :: conductTechnicalWrittenTest() - Java HireJavaTester :: conductTechnicalInterview() - Java HireTester :: conductHRTest() Candidate Hired
Key Observation

The client now calls only ONE method: tester.hire(). It does not know the step names, does not know the order, and does not know which steps are common vs specific. The hire() template method handles everything. Common steps (Aptitude, GD, HR) are printed from HireTester. Specific steps (Written, Interview) are printed from HireJavaTester.

4. Hook Methods

What is a Hook?

A hook method is a method in the abstract superclass that has a default implementation (usually empty or returning a default value). Subclasses can override it but are not required to. This is different from abstract methods which must be overridden.

Hooks let subclasses optionally plug into the algorithm at specific points without being forced to implement every step.

public abstract class DataProcessor {

    // Template method - fixed algorithm
    public final void process() {
        readData();
        if (shouldValidate()) {  // HOOK: subclass can override
            validateData();
        }
        transformData();
        saveData();
        if (shouldNotify()) {    // HOOK: subclass can override
            notifyCompletion();
        }
    }

    // Concrete steps
    protected void readData() {
        System.out.println("Reading data from source");
    }
    protected void saveData() {
        System.out.println("Saving processed data");
    }
    protected void notifyCompletion() {
        System.out.println("Sending completion notification");
    }

    // Abstract steps - subclass MUST implement
    protected abstract void validateData();
    protected abstract void transformData();

    // HOOK methods - subclass CAN override (default: true)
    protected boolean shouldValidate() { return true; }
    protected boolean shouldNotify() { return true; }
}

// Subclass that skips validation and notification
public class QuickProcessor extends DataProcessor {
    protected void validateData() { /* not called due to hook */ }
    protected void transformData() {
        System.out.println("QuickProcessor: fast transformation");
    }

    @Override
    protected boolean shouldValidate() { return false; }  // skip validation

    @Override
    protected boolean shouldNotify() { return false; }    // skip notification
}
Method TypeIn SuperclassIn SubclassRequired?
Concrete methodFully implementedInherited, not overriddenN/A
Abstract methodNo implementationMust overrideYes
Hook methodDefault implementationCan overrideNo

5. Pre-defined Template Method Examples

JDK Template Methods

Class/InterfaceTemplate MethodAbstract Steps
java.io.InputStreamread(byte[], int, int)read() (abstract, subclass provides byte reading)
java.util.AbstractListaddAll(), indexOf()get(int), size() (abstract)
java.util.AbstractSetequals(), removeAll()iterator(), size() (abstract)
java.util.AbstractMapcontainsKey(), get()entrySet() (abstract)
javax.servlet.http.HttpServletservice()doGet(), doPost(), doPut(), doDelete()

Spring Framework Template Classes

Template ClassAlgorithmWhat it hides
JdbcTemplateGet connection, create statement, execute, process results, closeConnection management, resource cleanup, exception translation
NamedParameterJdbcTemplateSame as JdbcTemplate with named parametersParameter binding by name instead of index
HibernateTemplateOpen session, begin transaction, execute, commit/rollback, closeSession and transaction management
RestTemplateCreate HTTP connection, send request, read response, closeHTTP connection management, serialization/deserialization
Spring JdbcTemplate Example
// Without template method (developer manages everything):
Connection conn = dataSource.getConnection();
try {
    PreparedStatement ps = conn.prepareStatement("SELECT * FROM accounts WHERE id = ?");
    ps.setString(1, accountId);
    ResultSet rs = ps.executeQuery();
    while (rs.next()) {
        // process each row
    }
    rs.close();
    ps.close();
} catch (SQLException e) {
    // handle exception
} finally {
    conn.close();  // must not forget!
}

// With JdbcTemplate (template method handles the boilerplate):
Account account = jdbcTemplate.queryForObject(
    "SELECT * FROM accounts WHERE id = ?",
    new Object[]{accountId},
    (rs, rowNum) -> new Account(
        rs.getString("id"),
        rs.getString("holder_name"),
        rs.getBigDecimal("balance")
    )
);
// Connection open/close, statement creation, exception handling -- all handled by JdbcTemplate

6. Template Method Combined with Factory Pattern

Template Method and Factory patterns work together naturally. The template method defines the algorithm. The factory creates the correct subclass. The client calls the factory, gets a tester, and calls the template method.

Client | |--> TesterFactory.getHireTester("java") [Factory Pattern] | returns HireJavaTester | |--> tester.hire() [Template Method] | |--> conductAptitudeTest() (common - from HireTester) |--> conductGroupDiscussion() (common - from HireTester) |--> conductTechnicalWrittenTest() (specific - from HireJavaTester) |--> conductTechnicalInterview() (specific - from HireJavaTester) |--> conductHRTest() (common - from HireTester)

This combination is seen in the BajajFactory example (Factory Method + Template Method) from the Factory pattern notes. The orderBike() method is a template method that calls createBike() (factory method) + painting() + assembling() + engineTest() + roadTest() in fixed order.

7. Full-Length Industry Project: Order Processing System

Business Scenario

National Bank of India processes three types of financial orders: Fund Transfer, Fixed Deposit, and Loan Application. Each order follows the same processing algorithm with 6 steps in a fixed order, but some steps differ per order type:

  1. Validate request (different validation rules per type)
  2. Check eligibility (different criteria per type)
  3. Calculate charges (different fee structure per type)
  4. Execute transaction (different processing logic per type)
  5. Generate receipt (common for all)
  6. Send notification (common for all, but can be skipped via hook)

Steps 1-4 are specific to each order type. Steps 5-6 are common. Step 6 can be optionally skipped using a hook method.

High-Level Design (HLD)

+---------------------------------------------------+ | CLIENT LAYER | | BankingApp (main) | | | | | +-- uses OrderProcessorFactory | | +-- calls processor.processOrder() | +----------------------------+----------------------+ | v +---------------------------------------------------+ | FACTORY LAYER | | OrderProcessorFactory | | +-- createProcessor("transfer") --> FundTransfer | | +-- createProcessor("fd") --> FixedDeposit | | +-- createProcessor("loan") --> LoanApplication| +---------------------------------------------------+ | v +---------------------------------------------------+ | TEMPLATE METHOD LAYER | | | | OrderProcessor (abstract) | | +-- processOrder() [FINAL TEMPLATE METHOD] | | | | | +--> validateRequest() [abstract] | | +--> checkEligibility() [abstract] | | +--> calculateCharges() [abstract] | | +--> executeTransaction() [abstract] | | +--> generateReceipt() [concrete] | | +--> if shouldNotify(): | | sendNotification() [concrete] | | +--> shouldNotify() [HOOK: default true] | | | FundTransferProcessor (extends OrderProcessor) | | FixedDepositProcessor (extends OrderProcessor) | | LoanApplicationProcessor(extends OrderProcessor) | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | OrderProcessor (abstract) | +-------------------------------------------------------+ | # orderId: String | | # amount: BigDecimal | | # customerId: String | +-------------------------------------------------------+ | + processOrder(): OrderResult [final template] | | # validateRequest(): void [abstract] | | # checkEligibility(): void [abstract] | | # calculateCharges(): BigDecimal [abstract] | | # executeTransaction(): String [abstract] | | # generateReceipt(): void [concrete] | | # sendNotification(): void [concrete] | | # shouldNotify(): boolean [hook, default true]| +-------------------------------------------------------+ ^ ^ ^ | | | +------------------+ +-----------------+ +--------------------+ | FundTransfer | | FixedDeposit | | LoanApplication | | Processor | | Processor | | Processor | +------------------+ +-----------------+ +--------------------+ | - toAccount | | - tenureMonths | | - loanType | | - ifscCode | | - interestRate | | - tenureYears | +------------------+ +-----------------+ +--------------------+ | validates: | | validates: | | validates: | | account, IFSC, | | min amount, | | credit score, | | balance | | tenure range | | income, age | +------------------+ +-----------------+ +--------------------+ +-------------------------------------------------------+ | OrderResult | +-------------------------------------------------------+ | - success: boolean | | - transactionId: String | | - charges: BigDecimal | | - message: String | +-------------------------------------------------------+

Project Structure

nbi-order-processing/ ├── src/ │ └── main/java/com/nbi/order/ │ ├── model/ │ │ └── OrderResult.java │ ├── processor/ │ │ ├── OrderProcessor.java (abstract, template method) │ │ ├── FundTransferProcessor.java │ │ ├── FixedDepositProcessor.java │ │ └── LoanApplicationProcessor.java │ ├── factory/ │ │ └── OrderProcessorFactory.java │ └── Application.java ├── src/ │ └── test/java/com/nbi/order/ │ ├── processor/ │ │ └── OrderProcessorTest.java │ └── factory/ │ └── OrderProcessorFactoryTest.java └── pom.xml

OrderResult.java

package com.nbi.order.model;

import java.math.BigDecimal;

public class OrderResult {
    private final boolean success;
    private final String transactionId;
    private final BigDecimal charges;
    private final String message;

    public OrderResult(boolean success, String transactionId,
                       BigDecimal charges, String message) {
        this.success = success;
        this.transactionId = transactionId;
        this.charges = charges;
        this.message = message;
    }

    public boolean isSuccess() { return success; }
    public String getTransactionId() { return transactionId; }
    public BigDecimal getCharges() { return charges; }
    public String getMessage() { return message; }

    @Override
    public String toString() {
        if (success) {
            return "SUCCESS | txn: " + transactionId
                + " | charges: Rs " + charges + " | " + message;
        }
        return "FAILED | " + message;
    }
}

OrderProcessor.java (abstract superclass with template method)

package com.nbi.order.processor;

import com.nbi.order.model.OrderResult;
import java.math.BigDecimal;
import java.util.UUID;

public abstract class OrderProcessor {
    protected final String orderId;
    protected final BigDecimal amount;
    protected final String customerId;

    protected OrderProcessor(String customerId, BigDecimal amount) {
        this.orderId = "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
        this.customerId = customerId;
        this.amount = amount;
    }

    /**
     * TEMPLATE METHOD: defines the fixed algorithm for processing any order.
     * Marked final so subclasses cannot change the order of steps.
     */
    public final OrderResult processOrder() {
        System.out.println("Processing order: " + orderId
            + " | Type: " + getOrderType()
            + " | Customer: " + customerId
            + " | Amount: Rs " + amount);

        try {
            // Step 1: Validate (specific per order type)
            validateRequest();
            System.out.println("  Step 1: Validation passed");

            // Step 2: Check eligibility (specific per order type)
            checkEligibility();
            System.out.println("  Step 2: Eligibility confirmed");

            // Step 3: Calculate charges (specific per order type)
            BigDecimal charges = calculateCharges();
            System.out.println("  Step 3: Charges calculated: Rs " + charges);

            // Step 4: Execute transaction (specific per order type)
            String txnId = executeTransaction();
            System.out.println("  Step 4: Transaction executed: " + txnId);

            // Step 5: Generate receipt (common)
            generateReceipt(txnId, charges);
            System.out.println("  Step 5: Receipt generated");

            // Step 6: Send notification (common, with hook)
            if (shouldNotify()) {
                sendNotification(txnId);
                System.out.println("  Step 6: Notification sent");
            } else {
                System.out.println("  Step 6: Notification skipped (hook)");
            }

            return new OrderResult(true, txnId, charges, getOrderType() + " completed");

        } catch (Exception e) {
            System.out.println("  FAILED: " + e.getMessage());
            return new OrderResult(false, null, BigDecimal.ZERO, e.getMessage());
        }
    }

    // Abstract steps: subclass MUST implement
    protected abstract void validateRequest();
    protected abstract void checkEligibility();
    protected abstract BigDecimal calculateCharges();
    protected abstract String executeTransaction();
    protected abstract String getOrderType();

    // Concrete steps: common implementation, shared by all
    protected void generateReceipt(String txnId, BigDecimal charges) {
        System.out.println("    Receipt: Order=" + orderId + " Txn=" + txnId
            + " Amount=Rs " + amount + " Charges=Rs " + charges);
    }

    protected void sendNotification(String txnId) {
        System.out.println("    SMS sent to customer " + customerId
            + ": Your " + getOrderType() + " (Txn: " + txnId + ") is complete.");
    }

    // HOOK method: subclass CAN override to skip notification
    protected boolean shouldNotify() {
        return true;
    }
}

FundTransferProcessor.java

package com.nbi.order.processor;

import java.math.BigDecimal;
import java.util.UUID;

public class FundTransferProcessor extends OrderProcessor {
    private final String toAccount;
    private final String ifscCode;

    public FundTransferProcessor(String customerId, BigDecimal amount,
                                  String toAccount, String ifscCode) {
        super(customerId, amount);
        this.toAccount = toAccount;
        this.ifscCode = ifscCode;
    }

    @Override
    protected void validateRequest() {
        if (toAccount == null || toAccount.isEmpty()) {
            throw new IllegalArgumentException("Beneficiary account is required");
        }
        if (ifscCode == null || ifscCode.length() != 11) {
            throw new IllegalArgumentException("Valid 11-char IFSC code is required");
        }
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Transfer amount must be positive");
        }
    }

    @Override
    protected void checkEligibility() {
        System.out.println("    Checking account balance for " + customerId);
        System.out.println("    Verifying beneficiary " + toAccount + " at " + ifscCode);
    }

    @Override
    protected BigDecimal calculateCharges() {
        // NEFT: Rs 5 for up to 10K, Rs 15 for 10K-1L, Rs 25 for above 1L
        if (amount.compareTo(new BigDecimal("10000")) <= 0) return new BigDecimal("5");
        if (amount.compareTo(new BigDecimal("100000")) <= 0) return new BigDecimal("15");
        return new BigDecimal("25");
    }

    @Override
    protected String executeTransaction() {
        String txnId = "NEFT" + UUID.randomUUID().toString().substring(0, 10).toUpperCase();
        System.out.println("    Debiting Rs " + amount + " from " + customerId);
        System.out.println("    Crediting Rs " + amount + " to " + toAccount);
        return txnId;
    }

    @Override
    protected String getOrderType() { return "Fund Transfer"; }
}

FixedDepositProcessor.java

package com.nbi.order.processor;

import java.math.BigDecimal;
import java.util.UUID;

public class FixedDepositProcessor extends OrderProcessor {
    private final int tenureMonths;
    private final double interestRate;

    public FixedDepositProcessor(String customerId, BigDecimal amount,
                                  int tenureMonths) {
        super(customerId, amount);
        this.tenureMonths = tenureMonths;
        // Rate depends on tenure
        this.interestRate = tenureMonths >= 12 ? 7.5 : (tenureMonths >= 6 ? 6.5 : 5.5);
    }

    @Override
    protected void validateRequest() {
        if (amount.compareTo(new BigDecimal("1000")) < 0) {
            throw new IllegalArgumentException("Minimum FD amount is Rs 1,000");
        }
        if (tenureMonths < 1 || tenureMonths > 120) {
            throw new IllegalArgumentException("Tenure must be between 1 and 120 months");
        }
    }

    @Override
    protected void checkEligibility() {
        System.out.println("    Checking KYC status for " + customerId);
        System.out.println("    FD tenure: " + tenureMonths + " months at " + interestRate + "%");
    }

    @Override
    protected BigDecimal calculateCharges() {
        // No charges for FD creation
        return BigDecimal.ZERO;
    }

    @Override
    protected String executeTransaction() {
        String fdNumber = "FD" + UUID.randomUUID().toString().substring(0, 10).toUpperCase();
        BigDecimal maturityAmount = amount.multiply(
            BigDecimal.valueOf(1 + (interestRate * tenureMonths) / (12 * 100)));
        System.out.println("    FD created: " + fdNumber);
        System.out.println("    Principal: Rs " + amount + " | Rate: " + interestRate
            + "% | Maturity: Rs " + maturityAmount.setScale(2, java.math.RoundingMode.HALF_UP));
        return fdNumber;
    }

    @Override
    protected String getOrderType() { return "Fixed Deposit"; }
}

LoanApplicationProcessor.java (overrides hook to skip notification)

package com.nbi.order.processor;

import java.math.BigDecimal;
import java.util.UUID;

public class LoanApplicationProcessor extends OrderProcessor {
    private final String loanType;
    private final int tenureYears;

    public LoanApplicationProcessor(String customerId, BigDecimal amount,
                                     String loanType, int tenureYears) {
        super(customerId, amount);
        this.loanType = loanType;
        this.tenureYears = tenureYears;
    }

    @Override
    protected void validateRequest() {
        if (amount.compareTo(new BigDecimal("50000")) < 0) {
            throw new IllegalArgumentException("Minimum loan amount is Rs 50,000");
        }
        if (tenureYears < 1 || tenureYears > 30) {
            throw new IllegalArgumentException("Loan tenure must be between 1 and 30 years");
        }
        if (loanType == null || loanType.isEmpty()) {
            throw new IllegalArgumentException("Loan type is required");
        }
    }

    @Override
    protected void checkEligibility() {
        System.out.println("    Checking credit score for " + customerId);
        System.out.println("    Verifying income documents");
        System.out.println("    Loan type: " + loanType + " | Tenure: " + tenureYears + " years");
    }

    @Override
    protected BigDecimal calculateCharges() {
        // Processing fee: 1% of loan amount, min Rs 500
        BigDecimal fee = amount.multiply(new BigDecimal("0.01"));
        return fee.compareTo(new BigDecimal("500")) < 0 ? new BigDecimal("500") : fee;
    }

    @Override
    protected String executeTransaction() {
        String loanId = "LN" + UUID.randomUUID().toString().substring(0, 10).toUpperCase();
        System.out.println("    Loan application " + loanId + " submitted for approval");
        System.out.println("    Amount: Rs " + amount + " | Type: " + loanType);
        return loanId;
    }

    @Override
    protected String getOrderType() { return "Loan Application"; }

    // HOOK override: loan applications go through approval, no immediate notification
    @Override
    protected boolean shouldNotify() {
        return false;
    }
}

OrderProcessorFactory.java

package com.nbi.order.factory;

import com.nbi.order.processor.*;
import java.math.BigDecimal;

public class OrderProcessorFactory {

    public static OrderProcessor createTransfer(String customerId, BigDecimal amount,
                                                 String toAccount, String ifscCode) {
        return new FundTransferProcessor(customerId, amount, toAccount, ifscCode);
    }

    public static OrderProcessor createFixedDeposit(String customerId, BigDecimal amount,
                                                     int tenureMonths) {
        return new FixedDepositProcessor(customerId, amount, tenureMonths);
    }

    public static OrderProcessor createLoanApplication(String customerId, BigDecimal amount,
                                                        String loanType, int tenureYears) {
        return new LoanApplicationProcessor(customerId, amount, loanType, tenureYears);
    }
}

Application.java

package com.nbi.order;

import com.nbi.order.factory.OrderProcessorFactory;
import com.nbi.order.model.OrderResult;
import com.nbi.order.processor.OrderProcessor;
import java.math.BigDecimal;

public class Application {
    public static void main(String[] args) {

        // Scenario 1: Fund Transfer
        System.out.println("=== Scenario 1: Fund Transfer ===");
        OrderProcessor transfer = OrderProcessorFactory.createTransfer(
            "CUST001", new BigDecimal("50000"),
            "SAV2001", "ICIC0001234");
        OrderResult r1 = transfer.processOrder();
        System.out.println("Result: " + r1);
        System.out.println();

        // Scenario 2: Fixed Deposit
        System.out.println("=== Scenario 2: Fixed Deposit ===");
        OrderProcessor fd = OrderProcessorFactory.createFixedDeposit(
            "CUST002", new BigDecimal("500000"), 24);
        OrderResult r2 = fd.processOrder();
        System.out.println("Result: " + r2);
        System.out.println();

        // Scenario 3: Loan Application (notification skipped via hook)
        System.out.println("=== Scenario 3: Loan Application ===");
        OrderProcessor loan = OrderProcessorFactory.createLoanApplication(
            "CUST003", new BigDecimal("2000000"),
            "Home Loan", 20);
        OrderResult r3 = loan.processOrder();
        System.out.println("Result: " + r3);
        System.out.println();

        // Scenario 4: Validation failure
        System.out.println("=== Scenario 4: Validation Failure ===");
        OrderProcessor badTransfer = OrderProcessorFactory.createTransfer(
            "CUST004", new BigDecimal("1000"),
            "", "INVALID");
        OrderResult r4 = badTransfer.processOrder();
        System.out.println("Result: " + r4);
        System.out.println();

        // Scenario 5: FD minimum amount failure
        System.out.println("=== Scenario 5: FD Minimum Amount ===");
        OrderProcessor badFd = OrderProcessorFactory.createFixedDeposit(
            "CUST005", new BigDecimal("500"), 12);
        OrderResult r5 = badFd.processOrder();
        System.out.println("Result: " + r5);
    }
}

Expected Output

=== Scenario 1: Fund Transfer === Processing order: ORD-A1B2C3D4 | Type: Fund Transfer | Customer: CUST001 | Amount: Rs 50000 Step 1: Validation passed Checking account balance for CUST001 Verifying beneficiary SAV2001 at ICIC0001234 Step 2: Eligibility confirmed Step 3: Charges calculated: Rs 15 Debiting Rs 50000 from CUST001 Crediting Rs 50000 to SAV2001 Step 4: Transaction executed: NEFT1A2B3C4D5E Receipt: Order=ORD-A1B2C3D4 Txn=NEFT1A2B3C4D5E Amount=Rs 50000 Charges=Rs 15 Step 5: Receipt generated SMS sent to customer CUST001: Your Fund Transfer (Txn: NEFT1A2B3C4D5E) is complete. Step 6: Notification sent Result: SUCCESS | txn: NEFT1A2B3C4D5E | charges: Rs 15 | Fund Transfer completed === Scenario 2: Fixed Deposit === Processing order: ORD-E5F6G7H8 | Type: Fixed Deposit | Customer: CUST002 | Amount: Rs 500000 Step 1: Validation passed Checking KYC status for CUST002 FD tenure: 24 months at 7.5% Step 2: Eligibility confirmed Step 3: Charges calculated: Rs 0 FD created: FDI9J0K1L2M3 Principal: Rs 500000 | Rate: 7.5% | Maturity: Rs 562500.00 Step 4: Transaction executed: FDI9J0K1L2M3 Receipt: Order=ORD-E5F6G7H8 Txn=FDI9J0K1L2M3 Amount=Rs 500000 Charges=Rs 0 Step 5: Receipt generated SMS sent to customer CUST002: Your Fixed Deposit (Txn: FDI9J0K1L2M3) is complete. Step 6: Notification sent Result: SUCCESS | txn: FDI9J0K1L2M3 | charges: Rs 0 | Fixed Deposit completed === Scenario 3: Loan Application === Processing order: ORD-N4O5P6Q7 | Type: Loan Application | Customer: CUST003 | Amount: Rs 2000000 Step 1: Validation passed Checking credit score for CUST003 Verifying income documents Loan type: Home Loan | Tenure: 20 years Step 2: Eligibility confirmed Step 3: Charges calculated: Rs 20000 Loan application LNR8S9T0U1V2 submitted for approval Amount: Rs 2000000 | Type: Home Loan Step 4: Transaction executed: LNR8S9T0U1V2 Receipt: Order=ORD-N4O5P6Q7 Txn=LNR8S9T0U1V2 Amount=Rs 2000000 Charges=Rs 20000 Step 5: Receipt generated Step 6: Notification skipped (hook) Result: SUCCESS | txn: LNR8S9T0U1V2 | charges: Rs 20000 | Loan Application completed === Scenario 4: Validation Failure === Processing order: ORD-W3X4Y5Z6 | Type: Fund Transfer | Customer: CUST004 | Amount: Rs 1000 FAILED: Beneficiary account is required Result: FAILED | Beneficiary account is required === Scenario 5: FD Minimum Amount === Processing order: ORD-A7B8C9D0 | Type: Fixed Deposit | Customer: CUST005 | Amount: Rs 500 FAILED: Minimum FD amount is Rs 1,000 Result: FAILED | Minimum FD amount is Rs 1,000

Key observations from the project

  1. One template method, three order types: processOrder() runs the same 6-step algorithm for transfers, FDs, and loans. The order of steps never changes.
  2. Each processor overrides only its specific steps: FundTransferProcessor validates accounts and IFSC. FixedDepositProcessor validates tenure and calculates interest. LoanApplicationProcessor checks credit score. None of them touch the common steps.
  3. Hook method in action: LoanApplicationProcessor overrides shouldNotify() to return false. The template method checks the hook and skips step 6. Other processors get the default (true) behavior.
  4. Validation failures stop the algorithm: If step 1 throws an exception, steps 2-6 never execute. The template method catches the exception and returns a failure result.
  5. Factory + Template Method combined: OrderProcessorFactory creates the right processor. The client calls processOrder(). One line runs the entire algorithm.
  6. Adding a new order type (e.g., Insurance Premium): Create InsurancePremiumProcessor extends OrderProcessor, implement the 4 abstract methods, add a factory method. No existing code changes.

8. When to Use and When NOT to Use

When to use Template Method

ScenarioExampleWhy Template Method
Fixed algorithm with varying stepsOrder processing, recruitment, data pipelineAlgorithm order is constant, specific steps differ per type
Avoiding code duplicationCommon steps across subclassesWrite once in superclass, inherit everywhere
Enforcing step orderDatabase migration (backup, migrate, verify, cleanup)final template method prevents subclasses from reordering
Framework/library designHttpServlet, JdbcTemplate, AbstractListFramework defines the skeleton, user fills in the specifics
Optional steps neededNotification, logging, cachingHook methods let subclasses opt in/out without changing the algorithm

When NOT to use Template Method

ScenarioWhy NOTUse Instead
All steps are differentNothing is common. Superclass would be empty.Strategy Pattern (swap entire algorithm)
Algorithm order variesTemplate method enforces fixed order. If order must change per subclass, it does not fit.Strategy or Chain of Responsibility
Only one implementationNo subclasses needed. Template method adds unnecessary inheritance.Simple method
Favor composition over inheritanceDeep inheritance hierarchies are hard to maintain.Strategy Pattern (composition-based)

Template Method vs Strategy Pattern

AspectTemplate MethodStrategy
MechanismInheritance (abstract class + subclasses)Composition (interface + implementations)
What variesSome steps of the algorithmThe entire algorithm
Order enforcedYes (final template method)No (each strategy defines its own flow)
Adding variantsNew subclassNew strategy implementation
Runtime switchDifficult (object type is fixed at creation)Easy (swap strategy object at runtime)

9. Quick Reference Card

ComponentRoleIn our project
Abstract classDefines template method + common steps + abstract steps + hooksOrderProcessor
Template methodfinal method that runs the fixed algorithmprocessOrder()
Concrete stepsCommon implementation in superclassgenerateReceipt(), sendNotification()
Abstract stepsSubclass must implementvalidateRequest(), calculateCharges(), etc.
Hook methodDefault implementation, subclass can overrideshouldNotify()
Concrete subclassFills in the specific stepsFundTransferProcessor, FixedDepositProcessor, LoanApplicationProcessor
Behavioral GOF Pattern Medium

Template Method Design Pattern in Java

Complete Tutorial | 11 Sections | Full Order Processing Project with HLD and LLD | Hook Methods | Spring Templates | Multiple Scenarios | 12 Interview Questions

1. What is Template Method Pattern?

Definition

The Template Method pattern is a GOF Behavioral Pattern. Template means blueprint or preset format. It defines the skeleton of an algorithm in a method (the template method), deferring some steps to subclasses. Subclasses can redefine certain steps of the algorithm without changing the algorithm's overall structure and step order.

One line: Fix the algorithm order in the superclass, let subclasses fill in the details.

The template method DP is a superclass/framework that defines the skeleton of operations. Common steps are implemented in the superclass. Specific steps are declared as abstract methods and implemented in subclasses. The order of steps is locked in a final template method so no subclass can change it.

Real-world analogy

Fresher Recruitment Drive at CMR College: The recruitment process for Java, DotNet, and PHP freshers follows the same algorithm with 5 steps in a fixed order:

  1. Aptitude Test (common for all)
  2. Group Discussion (common for all)
  3. Technical Written Test (differs per technology: Java / DotNet / PHP)
  4. Technical Interview (differs per technology)
  5. HR Test (common for all)

The common steps are identical regardless of technology. Only steps 3 and 4 vary. The order never changes. This is a template method.

2. The Problem

Problem

When each subclass independently implements the full algorithm:

  • Common steps (Aptitude, GD, HR) are duplicated in every class
  • The order of steps is not enforced. Each client must remember the correct sequence
  • The full algorithm burden is placed on the client. A single client mistake breaks the process
  • All firms, services, and implementations repeat the same sequence of operations that could be completed by taking support of a template

Problem code without template method

JavaTester.java (all 5 steps duplicated)

package com.tt.recruitment;

public class JavaTester {
    public boolean conductAptitudeTest() {
        System.out.println("JavaTester :: conductAptitudeTest()");
        return true;
    }
    public boolean conductGroupDiscussion() {
        System.out.println("JavaTester :: conductGroupDiscussion()");
        return true;
    }
    public boolean conductTechnicalWrittenTest() {
        System.out.println("JavaTester :: conductTechnicalWrittenTest() - Java");
        return true;
    }
    public boolean conductTechnicalInterview() {
        System.out.println("JavaTester :: conductTechnicalInterview() - Java");
        return true;
    }
    public boolean conductHRTest() {
        System.out.println("JavaTester :: conductHRTest()");
        return true;
    }
}

DotNetTester.java (same 5 steps duplicated again)

package com.tt.recruitment;

public class DotNetTester {
    public boolean conductAptitudeTest() {
        System.out.println("DotNetTester :: conductAptitudeTest()");
        return true;
    }
    public boolean conductGroupDiscussion() {
        System.out.println("DotNetTester :: conductGroupDiscussion()");
        return true;
    }
    public boolean conductTechnicalWrittenTest() {
        System.out.println("DotNetTester :: conductTechnicalWrittenTest() - DotNet");
        return true;
    }
    public boolean conductTechnicalInterview() {
        System.out.println("DotNetTester :: conductTechnicalInterview() - DotNet");
        return true;
    }
    public boolean conductHRTest() {
        System.out.println("DotNetTester :: conductHRTest()");
        return true;
    }
}

Problem client code (client must know all steps and their order)

// Client1.java
package com.tt.test;

import com.tt.recruitment.JavaTester;

public class Client1 {
    public static void main(String[] args) {
        JavaTester tester = new JavaTester();

        // Client must remember the FULL algorithm AND correct order
        boolean flag = tester.conductAptitudeTest();
        if (flag) flag = tester.conductGroupDiscussion();
        if (flag) flag = tester.conductTechnicalWrittenTest();
        if (flag) flag = tester.conductTechnicalInterview();
        if (flag) flag = tester.conductHRTest();

        System.out.println(flag ? "Candidate Hired" : "Candidate Not Hired");
    }
}

// Client2.java - same algorithm + order duplicated AGAIN
package com.tt.test;

import com.tt.recruitment.DotNetTester;

public class Client2 {
    public static void main(String[] args) {
        DotNetTester tester = new DotNetTester();

        boolean flag = tester.conductAptitudeTest();
        if (flag) flag = tester.conductGroupDiscussion();
        if (flag) flag = tester.conductTechnicalWrittenTest();
        if (flag) flag = tester.conductTechnicalInterview();
        if (flag) flag = tester.conductHRTest();

        System.out.println(flag ? "Candidate Hired" : "Candidate Not Hired");
    }
}
Problem: each class is separate, no enforced order, code duplication JavaTester DotNetTester PhpTester +-----------+ +-----------+ +-----------+ | aptitude | | aptitude | | aptitude | | gd | SAME CODE | gd | SAME CODE | gd | | written | DUPLICATED | written | DUPLICATED | written | | interview | | interview | | interview | | hr | | hr | | hr | +-----------+ +-----------+ +-----------+ Client must call all 5 methods in exact order. If client makes a mistake in order --> recruitment process is wrong.

3. The Solution: Template Method

Solution

Create an abstract superclass with:

  1. Common steps as concrete methods (implemented once)
  2. Specific steps as abstract methods (subclasses implement)
  3. Template method marked final that calls all steps in the fixed order

The client calls only the template method. One call runs the entire algorithm.

HireTester (abstract superclass) +-- conductAptitudeTest() [concrete - common, implemented here] +-- conductGroupDiscussion() [concrete - common, implemented here] +-- conductTechnicalWrittenTest() [ABSTRACT - specific, subclass implements] +-- conductTechnicalInterview() [ABSTRACT - specific, subclass implements] +-- conductHRTest() [concrete - common, implemented here] +-- hire() [FINAL TEMPLATE METHOD - fixed order] | +---- HireJavaTester (implements written + interview for Java) +---- HireDotNetTester (implements written + interview for DotNet) +---- HirePhpTester (implements written + interview for PHP)

HireTester.java (abstract superclass with template method)

package com.tt.recruitment;

public abstract class HireTester {

    // COMMON steps - implemented once, shared by all subclasses
    public boolean conductAptitudeTest() {
        System.out.println("HireTester :: conductAptitudeTest()");
        return true;
    }

    public boolean conductGroupDiscussion() {
        System.out.println("HireTester :: conductGroupDiscussion()");
        return true;
    }

    public boolean conductHRTest() {
        System.out.println("HireTester :: conductHRTest()");
        return true;
    }

    // SPECIFIC steps - abstract, each technology implements its own
    public abstract boolean conductTechnicalWrittenTest();
    public abstract boolean conductTechnicalInterview();

    // TEMPLATE METHOD - final: order cannot be changed by subclass or client
    public final boolean hire() {
        boolean flag = conductAptitudeTest();
        if (flag) flag = conductGroupDiscussion();
        if (flag) flag = conductTechnicalWrittenTest();
        if (flag) flag = conductTechnicalInterview();
        if (flag) flag = conductHRTest();
        return flag;
    }
}

Concrete subclasses

// HireJavaTester.java
package com.tt.recruitment;

public class HireJavaTester extends HireTester {

    @Override
    public boolean conductTechnicalWrittenTest() {
        System.out.println("HireJavaTester :: conductTechnicalWrittenTest() - Java");
        return true;
    }

    @Override
    public boolean conductTechnicalInterview() {
        System.out.println("HireJavaTester :: conductTechnicalInterview() - Java");
        return true;
    }
}

// HireDotNetTester.java
package com.tt.recruitment;

public class HireDotNetTester extends HireTester {

    @Override
    public boolean conductTechnicalWrittenTest() {
        System.out.println("HireDotNetTester :: conductTechnicalWrittenTest() - DotNet");
        return true;
    }

    @Override
    public boolean conductTechnicalInterview() {
        System.out.println("HireDotNetTester :: conductTechnicalInterview() - DotNet");
        return true;
    }
}

// HirePhpTester.java
package com.tt.recruitment;

public class HirePhpTester extends HireTester {

    @Override
    public boolean conductTechnicalWrittenTest() {
        System.out.println("HirePhpTester :: conductTechnicalWrittenTest() - PHP");
        return true;
    }

    @Override
    public boolean conductTechnicalInterview() {
        System.out.println("HirePhpTester :: conductTechnicalInterview() - PHP");
        return true;
    }
}

Factory to create testers

package com.tt.factory;

import com.tt.recruitment.*;

public class TesterFactory {
    public static HireTester getHireTester(String type) {
        if (type == null) {
            throw new IllegalArgumentException("Tester type cannot be null");
        }
        if (type.equalsIgnoreCase("java")) return new HireJavaTester();
        if (type.equalsIgnoreCase("dotnet")) return new HireDotNetTester();
        if (type.equalsIgnoreCase("php")) return new HirePhpTester();
        throw new IllegalArgumentException("Invalid tester type: " + type);
    }
}

Client code (single call runs entire algorithm)

// CmrApp.java
package com.tt.test;

import com.tt.recruitment.HireTester;
import com.tt.factory.TesterFactory;

public class CmrApp {
    public static void main(String[] args) {
        // Get the proper tester from factory
        HireTester tester = TesterFactory.getHireTester("java");

        // SINGLE call runs the whole fixed algorithm
        boolean result = tester.hire();
        System.out.println(result ? "Candidate Hired" : "Candidate Not Hired");
    }
}
HireTester :: conductAptitudeTest() HireTester :: conductGroupDiscussion() HireJavaTester :: conductTechnicalWrittenTest() - Java HireJavaTester :: conductTechnicalInterview() - Java HireTester :: conductHRTest() Candidate Hired
Key Observation

The client now calls only ONE method: tester.hire(). It does not know the step names, does not know the order, and does not know which steps are common vs specific. The hire() template method handles everything. Common steps (Aptitude, GD, HR) are printed from HireTester. Specific steps (Written, Interview) are printed from HireJavaTester.

4. Hook Methods

What is a Hook?

A hook method is a method in the abstract superclass that has a default implementation (usually empty or returning a default value). Subclasses can override it but are not required to. This is different from abstract methods which must be overridden.

Hooks let subclasses optionally plug into the algorithm at specific points without being forced to implement every step.

public abstract class DataProcessor {

    // Template method - fixed algorithm
    public final void process() {
        readData();
        if (shouldValidate()) {  // HOOK: subclass can override
            validateData();
        }
        transformData();
        saveData();
        if (shouldNotify()) {    // HOOK: subclass can override
            notifyCompletion();
        }
    }

    // Concrete steps
    protected void readData() {
        System.out.println("Reading data from source");
    }
    protected void saveData() {
        System.out.println("Saving processed data");
    }
    protected void notifyCompletion() {
        System.out.println("Sending completion notification");
    }

    // Abstract steps - subclass MUST implement
    protected abstract void validateData();
    protected abstract void transformData();

    // HOOK methods - subclass CAN override (default: true)
    protected boolean shouldValidate() { return true; }
    protected boolean shouldNotify() { return true; }
}

// Subclass that skips validation and notification
public class QuickProcessor extends DataProcessor {
    protected void validateData() { /* not called due to hook */ }
    protected void transformData() {
        System.out.println("QuickProcessor: fast transformation");
    }

    @Override
    protected boolean shouldValidate() { return false; }  // skip validation

    @Override
    protected boolean shouldNotify() { return false; }    // skip notification
}
Method TypeIn SuperclassIn SubclassRequired?
Concrete methodFully implementedInherited, not overriddenN/A
Abstract methodNo implementationMust overrideYes
Hook methodDefault implementationCan overrideNo

5. Pre-defined Template Method Examples

JDK Template Methods

Class/InterfaceTemplate MethodAbstract Steps
java.io.InputStreamread(byte[], int, int)read() (abstract, subclass provides byte reading)
java.util.AbstractListaddAll(), indexOf()get(int), size() (abstract)
java.util.AbstractSetequals(), removeAll()iterator(), size() (abstract)
java.util.AbstractMapcontainsKey(), get()entrySet() (abstract)
javax.servlet.http.HttpServletservice()doGet(), doPost(), doPut(), doDelete()

Spring Framework Template Classes

Template ClassAlgorithmWhat it hides
JdbcTemplateGet connection, create statement, execute, process results, closeConnection management, resource cleanup, exception translation
NamedParameterJdbcTemplateSame as JdbcTemplate with named parametersParameter binding by name instead of index
HibernateTemplateOpen session, begin transaction, execute, commit/rollback, closeSession and transaction management
RestTemplateCreate HTTP connection, send request, read response, closeHTTP connection management, serialization/deserialization
Spring JdbcTemplate Example
// Without template method (developer manages everything):
Connection conn = dataSource.getConnection();
try {
    PreparedStatement ps = conn.prepareStatement("SELECT * FROM accounts WHERE id = ?");
    ps.setString(1, accountId);
    ResultSet rs = ps.executeQuery();
    while (rs.next()) {
        // process each row
    }
    rs.close();
    ps.close();
} catch (SQLException e) {
    // handle exception
} finally {
    conn.close();  // must not forget!
}

// With JdbcTemplate (template method handles the boilerplate):
Account account = jdbcTemplate.queryForObject(
    "SELECT * FROM accounts WHERE id = ?",
    new Object[]{accountId},
    (rs, rowNum) -> new Account(
        rs.getString("id"),
        rs.getString("holder_name"),
        rs.getBigDecimal("balance")
    )
);
// Connection open/close, statement creation, exception handling -- all handled by JdbcTemplate

6. Template Method Combined with Factory Pattern

Template Method and Factory patterns work together naturally. The template method defines the algorithm. The factory creates the correct subclass. The client calls the factory, gets a tester, and calls the template method.

Client | |--> TesterFactory.getHireTester("java") [Factory Pattern] | returns HireJavaTester | |--> tester.hire() [Template Method] | |--> conductAptitudeTest() (common - from HireTester) |--> conductGroupDiscussion() (common - from HireTester) |--> conductTechnicalWrittenTest() (specific - from HireJavaTester) |--> conductTechnicalInterview() (specific - from HireJavaTester) |--> conductHRTest() (common - from HireTester)

This combination is seen in the BajajFactory example (Factory Method + Template Method) from the Factory pattern notes. The orderBike() method is a template method that calls createBike() (factory method) + painting() + assembling() + engineTest() + roadTest() in fixed order.

7. Full-Length Industry Project: Order Processing System

Business Scenario

National Bank of India processes three types of financial orders: Fund Transfer, Fixed Deposit, and Loan Application. Each order follows the same processing algorithm with 6 steps in a fixed order, but some steps differ per order type:

  1. Validate request (different validation rules per type)
  2. Check eligibility (different criteria per type)
  3. Calculate charges (different fee structure per type)
  4. Execute transaction (different processing logic per type)
  5. Generate receipt (common for all)
  6. Send notification (common for all, but can be skipped via hook)

Steps 1-4 are specific to each order type. Steps 5-6 are common. Step 6 can be optionally skipped using a hook method.

High-Level Design (HLD)

+---------------------------------------------------+ | CLIENT LAYER | | BankingApp (main) | | | | | +-- uses OrderProcessorFactory | | +-- calls processor.processOrder() | +----------------------------+----------------------+ | v +---------------------------------------------------+ | FACTORY LAYER | | OrderProcessorFactory | | +-- createProcessor("transfer") --> FundTransfer | | +-- createProcessor("fd") --> FixedDeposit | | +-- createProcessor("loan") --> LoanApplication| +---------------------------------------------------+ | v +---------------------------------------------------+ | TEMPLATE METHOD LAYER | | | | OrderProcessor (abstract) | | +-- processOrder() [FINAL TEMPLATE METHOD] | | | | | +--> validateRequest() [abstract] | | +--> checkEligibility() [abstract] | | +--> calculateCharges() [abstract] | | +--> executeTransaction() [abstract] | | +--> generateReceipt() [concrete] | | +--> if shouldNotify(): | | sendNotification() [concrete] | | +--> shouldNotify() [HOOK: default true] | | | FundTransferProcessor (extends OrderProcessor) | | FixedDepositProcessor (extends OrderProcessor) | | LoanApplicationProcessor(extends OrderProcessor) | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | OrderProcessor (abstract) | +-------------------------------------------------------+ | # orderId: String | | # amount: BigDecimal | | # customerId: String | +-------------------------------------------------------+ | + processOrder(): OrderResult [final template] | | # validateRequest(): void [abstract] | | # checkEligibility(): void [abstract] | | # calculateCharges(): BigDecimal [abstract] | | # executeTransaction(): String [abstract] | | # generateReceipt(): void [concrete] | | # sendNotification(): void [concrete] | | # shouldNotify(): boolean [hook, default true]| +-------------------------------------------------------+ ^ ^ ^ | | | +------------------+ +-----------------+ +--------------------+ | FundTransfer | | FixedDeposit | | LoanApplication | | Processor | | Processor | | Processor | +------------------+ +-----------------+ +--------------------+ | - toAccount | | - tenureMonths | | - loanType | | - ifscCode | | - interestRate | | - tenureYears | +------------------+ +-----------------+ +--------------------+ | validates: | | validates: | | validates: | | account, IFSC, | | min amount, | | credit score, | | balance | | tenure range | | income, age | +------------------+ +-----------------+ +--------------------+ +-------------------------------------------------------+ | OrderResult | +-------------------------------------------------------+ | - success: boolean | | - transactionId: String | | - charges: BigDecimal | | - message: String | +-------------------------------------------------------+

Project Structure

nbi-order-processing/ ├── src/ │ └── main/java/com/nbi/order/ │ ├── model/ │ │ └── OrderResult.java │ ├── processor/ │ │ ├── OrderProcessor.java (abstract, template method) │ │ ├── FundTransferProcessor.java │ │ ├── FixedDepositProcessor.java │ │ └── LoanApplicationProcessor.java │ ├── factory/ │ │ └── OrderProcessorFactory.java │ └── Application.java ├── src/ │ └── test/java/com/nbi/order/ │ ├── processor/ │ │ └── OrderProcessorTest.java │ └── factory/ │ └── OrderProcessorFactoryTest.java └── pom.xml

OrderResult.java

package com.nbi.order.model;

import java.math.BigDecimal;

public class OrderResult {
    private final boolean success;
    private final String transactionId;
    private final BigDecimal charges;
    private final String message;

    public OrderResult(boolean success, String transactionId,
                       BigDecimal charges, String message) {
        this.success = success;
        this.transactionId = transactionId;
        this.charges = charges;
        this.message = message;
    }

    public boolean isSuccess() { return success; }
    public String getTransactionId() { return transactionId; }
    public BigDecimal getCharges() { return charges; }
    public String getMessage() { return message; }

    @Override
    public String toString() {
        if (success) {
            return "SUCCESS | txn: " + transactionId
                + " | charges: Rs " + charges + " | " + message;
        }
        return "FAILED | " + message;
    }
}

OrderProcessor.java (abstract superclass with template method)

package com.nbi.order.processor;

import com.nbi.order.model.OrderResult;
import java.math.BigDecimal;
import java.util.UUID;

public abstract class OrderProcessor {
    protected final String orderId;
    protected final BigDecimal amount;
    protected final String customerId;

    protected OrderProcessor(String customerId, BigDecimal amount) {
        this.orderId = "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
        this.customerId = customerId;
        this.amount = amount;
    }

    /**
     * TEMPLATE METHOD: defines the fixed algorithm for processing any order.
     * Marked final so subclasses cannot change the order of steps.
     */
    public final OrderResult processOrder() {
        System.out.println("Processing order: " + orderId
            + " | Type: " + getOrderType()
            + " | Customer: " + customerId
            + " | Amount: Rs " + amount);

        try {
            // Step 1: Validate (specific per order type)
            validateRequest();
            System.out.println("  Step 1: Validation passed");

            // Step 2: Check eligibility (specific per order type)
            checkEligibility();
            System.out.println("  Step 2: Eligibility confirmed");

            // Step 3: Calculate charges (specific per order type)
            BigDecimal charges = calculateCharges();
            System.out.println("  Step 3: Charges calculated: Rs " + charges);

            // Step 4: Execute transaction (specific per order type)
            String txnId = executeTransaction();
            System.out.println("  Step 4: Transaction executed: " + txnId);

            // Step 5: Generate receipt (common)
            generateReceipt(txnId, charges);
            System.out.println("  Step 5: Receipt generated");

            // Step 6: Send notification (common, with hook)
            if (shouldNotify()) {
                sendNotification(txnId);
                System.out.println("  Step 6: Notification sent");
            } else {
                System.out.println("  Step 6: Notification skipped (hook)");
            }

            return new OrderResult(true, txnId, charges, getOrderType() + " completed");

        } catch (Exception e) {
            System.out.println("  FAILED: " + e.getMessage());
            return new OrderResult(false, null, BigDecimal.ZERO, e.getMessage());
        }
    }

    // Abstract steps: subclass MUST implement
    protected abstract void validateRequest();
    protected abstract void checkEligibility();
    protected abstract BigDecimal calculateCharges();
    protected abstract String executeTransaction();
    protected abstract String getOrderType();

    // Concrete steps: common implementation, shared by all
    protected void generateReceipt(String txnId, BigDecimal charges) {
        System.out.println("    Receipt: Order=" + orderId + " Txn=" + txnId
            + " Amount=Rs " + amount + " Charges=Rs " + charges);
    }

    protected void sendNotification(String txnId) {
        System.out.println("    SMS sent to customer " + customerId
            + ": Your " + getOrderType() + " (Txn: " + txnId + ") is complete.");
    }

    // HOOK method: subclass CAN override to skip notification
    protected boolean shouldNotify() {
        return true;
    }
}

FundTransferProcessor.java

package com.nbi.order.processor;

import java.math.BigDecimal;
import java.util.UUID;

public class FundTransferProcessor extends OrderProcessor {
    private final String toAccount;
    private final String ifscCode;

    public FundTransferProcessor(String customerId, BigDecimal amount,
                                  String toAccount, String ifscCode) {
        super(customerId, amount);
        this.toAccount = toAccount;
        this.ifscCode = ifscCode;
    }

    @Override
    protected void validateRequest() {
        if (toAccount == null || toAccount.isEmpty()) {
            throw new IllegalArgumentException("Beneficiary account is required");
        }
        if (ifscCode == null || ifscCode.length() != 11) {
            throw new IllegalArgumentException("Valid 11-char IFSC code is required");
        }
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Transfer amount must be positive");
        }
    }

    @Override
    protected void checkEligibility() {
        System.out.println("    Checking account balance for " + customerId);
        System.out.println("    Verifying beneficiary " + toAccount + " at " + ifscCode);
    }

    @Override
    protected BigDecimal calculateCharges() {
        // NEFT: Rs 5 for up to 10K, Rs 15 for 10K-1L, Rs 25 for above 1L
        if (amount.compareTo(new BigDecimal("10000")) <= 0) return new BigDecimal("5");
        if (amount.compareTo(new BigDecimal("100000")) <= 0) return new BigDecimal("15");
        return new BigDecimal("25");
    }

    @Override
    protected String executeTransaction() {
        String txnId = "NEFT" + UUID.randomUUID().toString().substring(0, 10).toUpperCase();
        System.out.println("    Debiting Rs " + amount + " from " + customerId);
        System.out.println("    Crediting Rs " + amount + " to " + toAccount);
        return txnId;
    }

    @Override
    protected String getOrderType() { return "Fund Transfer"; }
}

FixedDepositProcessor.java

package com.nbi.order.processor;

import java.math.BigDecimal;
import java.util.UUID;

public class FixedDepositProcessor extends OrderProcessor {
    private final int tenureMonths;
    private final double interestRate;

    public FixedDepositProcessor(String customerId, BigDecimal amount,
                                  int tenureMonths) {
        super(customerId, amount);
        this.tenureMonths = tenureMonths;
        // Rate depends on tenure
        this.interestRate = tenureMonths >= 12 ? 7.5 : (tenureMonths >= 6 ? 6.5 : 5.5);
    }

    @Override
    protected void validateRequest() {
        if (amount.compareTo(new BigDecimal("1000")) < 0) {
            throw new IllegalArgumentException("Minimum FD amount is Rs 1,000");
        }
        if (tenureMonths < 1 || tenureMonths > 120) {
            throw new IllegalArgumentException("Tenure must be between 1 and 120 months");
        }
    }

    @Override
    protected void checkEligibility() {
        System.out.println("    Checking KYC status for " + customerId);
        System.out.println("    FD tenure: " + tenureMonths + " months at " + interestRate + "%");
    }

    @Override
    protected BigDecimal calculateCharges() {
        // No charges for FD creation
        return BigDecimal.ZERO;
    }

    @Override
    protected String executeTransaction() {
        String fdNumber = "FD" + UUID.randomUUID().toString().substring(0, 10).toUpperCase();
        BigDecimal maturityAmount = amount.multiply(
            BigDecimal.valueOf(1 + (interestRate * tenureMonths) / (12 * 100)));
        System.out.println("    FD created: " + fdNumber);
        System.out.println("    Principal: Rs " + amount + " | Rate: " + interestRate
            + "% | Maturity: Rs " + maturityAmount.setScale(2, java.math.RoundingMode.HALF_UP));
        return fdNumber;
    }

    @Override
    protected String getOrderType() { return "Fixed Deposit"; }
}

LoanApplicationProcessor.java (overrides hook to skip notification)

package com.nbi.order.processor;

import java.math.BigDecimal;
import java.util.UUID;

public class LoanApplicationProcessor extends OrderProcessor {
    private final String loanType;
    private final int tenureYears;

    public LoanApplicationProcessor(String customerId, BigDecimal amount,
                                     String loanType, int tenureYears) {
        super(customerId, amount);
        this.loanType = loanType;
        this.tenureYears = tenureYears;
    }

    @Override
    protected void validateRequest() {
        if (amount.compareTo(new BigDecimal("50000")) < 0) {
            throw new IllegalArgumentException("Minimum loan amount is Rs 50,000");
        }
        if (tenureYears < 1 || tenureYears > 30) {
            throw new IllegalArgumentException("Loan tenure must be between 1 and 30 years");
        }
        if (loanType == null || loanType.isEmpty()) {
            throw new IllegalArgumentException("Loan type is required");
        }
    }

    @Override
    protected void checkEligibility() {
        System.out.println("    Checking credit score for " + customerId);
        System.out.println("    Verifying income documents");
        System.out.println("    Loan type: " + loanType + " | Tenure: " + tenureYears + " years");
    }

    @Override
    protected BigDecimal calculateCharges() {
        // Processing fee: 1% of loan amount, min Rs 500
        BigDecimal fee = amount.multiply(new BigDecimal("0.01"));
        return fee.compareTo(new BigDecimal("500")) < 0 ? new BigDecimal("500") : fee;
    }

    @Override
    protected String executeTransaction() {
        String loanId = "LN" + UUID.randomUUID().toString().substring(0, 10).toUpperCase();
        System.out.println("    Loan application " + loanId + " submitted for approval");
        System.out.println("    Amount: Rs " + amount + " | Type: " + loanType);
        return loanId;
    }

    @Override
    protected String getOrderType() { return "Loan Application"; }

    // HOOK override: loan applications go through approval, no immediate notification
    @Override
    protected boolean shouldNotify() {
        return false;
    }
}

OrderProcessorFactory.java

package com.nbi.order.factory;

import com.nbi.order.processor.*;
import java.math.BigDecimal;

public class OrderProcessorFactory {

    public static OrderProcessor createTransfer(String customerId, BigDecimal amount,
                                                 String toAccount, String ifscCode) {
        return new FundTransferProcessor(customerId, amount, toAccount, ifscCode);
    }

    public static OrderProcessor createFixedDeposit(String customerId, BigDecimal amount,
                                                     int tenureMonths) {
        return new FixedDepositProcessor(customerId, amount, tenureMonths);
    }

    public static OrderProcessor createLoanApplication(String customerId, BigDecimal amount,
                                                        String loanType, int tenureYears) {
        return new LoanApplicationProcessor(customerId, amount, loanType, tenureYears);
    }
}

Application.java

package com.nbi.order;

import com.nbi.order.factory.OrderProcessorFactory;
import com.nbi.order.model.OrderResult;
import com.nbi.order.processor.OrderProcessor;
import java.math.BigDecimal;

public class Application {
    public static void main(String[] args) {

        // Scenario 1: Fund Transfer
        System.out.println("=== Scenario 1: Fund Transfer ===");
        OrderProcessor transfer = OrderProcessorFactory.createTransfer(
            "CUST001", new BigDecimal("50000"),
            "SAV2001", "ICIC0001234");
        OrderResult r1 = transfer.processOrder();
        System.out.println("Result: " + r1);
        System.out.println();

        // Scenario 2: Fixed Deposit
        System.out.println("=== Scenario 2: Fixed Deposit ===");
        OrderProcessor fd = OrderProcessorFactory.createFixedDeposit(
            "CUST002", new BigDecimal("500000"), 24);
        OrderResult r2 = fd.processOrder();
        System.out.println("Result: " + r2);
        System.out.println();

        // Scenario 3: Loan Application (notification skipped via hook)
        System.out.println("=== Scenario 3: Loan Application ===");
        OrderProcessor loan = OrderProcessorFactory.createLoanApplication(
            "CUST003", new BigDecimal("2000000"),
            "Home Loan", 20);
        OrderResult r3 = loan.processOrder();
        System.out.println("Result: " + r3);
        System.out.println();

        // Scenario 4: Validation failure
        System.out.println("=== Scenario 4: Validation Failure ===");
        OrderProcessor badTransfer = OrderProcessorFactory.createTransfer(
            "CUST004", new BigDecimal("1000"),
            "", "INVALID");
        OrderResult r4 = badTransfer.processOrder();
        System.out.println("Result: " + r4);
        System.out.println();

        // Scenario 5: FD minimum amount failure
        System.out.println("=== Scenario 5: FD Minimum Amount ===");
        OrderProcessor badFd = OrderProcessorFactory.createFixedDeposit(
            "CUST005", new BigDecimal("500"), 12);
        OrderResult r5 = badFd.processOrder();
        System.out.println("Result: " + r5);
    }
}

Expected Output

=== Scenario 1: Fund Transfer === Processing order: ORD-A1B2C3D4 | Type: Fund Transfer | Customer: CUST001 | Amount: Rs 50000 Step 1: Validation passed Checking account balance for CUST001 Verifying beneficiary SAV2001 at ICIC0001234 Step 2: Eligibility confirmed Step 3: Charges calculated: Rs 15 Debiting Rs 50000 from CUST001 Crediting Rs 50000 to SAV2001 Step 4: Transaction executed: NEFT1A2B3C4D5E Receipt: Order=ORD-A1B2C3D4 Txn=NEFT1A2B3C4D5E Amount=Rs 50000 Charges=Rs 15 Step 5: Receipt generated SMS sent to customer CUST001: Your Fund Transfer (Txn: NEFT1A2B3C4D5E) is complete. Step 6: Notification sent Result: SUCCESS | txn: NEFT1A2B3C4D5E | charges: Rs 15 | Fund Transfer completed === Scenario 2: Fixed Deposit === Processing order: ORD-E5F6G7H8 | Type: Fixed Deposit | Customer: CUST002 | Amount: Rs 500000 Step 1: Validation passed Checking KYC status for CUST002 FD tenure: 24 months at 7.5% Step 2: Eligibility confirmed Step 3: Charges calculated: Rs 0 FD created: FDI9J0K1L2M3 Principal: Rs 500000 | Rate: 7.5% | Maturity: Rs 562500.00 Step 4: Transaction executed: FDI9J0K1L2M3 Receipt: Order=ORD-E5F6G7H8 Txn=FDI9J0K1L2M3 Amount=Rs 500000 Charges=Rs 0 Step 5: Receipt generated SMS sent to customer CUST002: Your Fixed Deposit (Txn: FDI9J0K1L2M3) is complete. Step 6: Notification sent Result: SUCCESS | txn: FDI9J0K1L2M3 | charges: Rs 0 | Fixed Deposit completed === Scenario 3: Loan Application === Processing order: ORD-N4O5P6Q7 | Type: Loan Application | Customer: CUST003 | Amount: Rs 2000000 Step 1: Validation passed Checking credit score for CUST003 Verifying income documents Loan type: Home Loan | Tenure: 20 years Step 2: Eligibility confirmed Step 3: Charges calculated: Rs 20000 Loan application LNR8S9T0U1V2 submitted for approval Amount: Rs 2000000 | Type: Home Loan Step 4: Transaction executed: LNR8S9T0U1V2 Receipt: Order=ORD-N4O5P6Q7 Txn=LNR8S9T0U1V2 Amount=Rs 2000000 Charges=Rs 20000 Step 5: Receipt generated Step 6: Notification skipped (hook) Result: SUCCESS | txn: LNR8S9T0U1V2 | charges: Rs 20000 | Loan Application completed === Scenario 4: Validation Failure === Processing order: ORD-W3X4Y5Z6 | Type: Fund Transfer | Customer: CUST004 | Amount: Rs 1000 FAILED: Beneficiary account is required Result: FAILED | Beneficiary account is required === Scenario 5: FD Minimum Amount === Processing order: ORD-A7B8C9D0 | Type: Fixed Deposit | Customer: CUST005 | Amount: Rs 500 FAILED: Minimum FD amount is Rs 1,000 Result: FAILED | Minimum FD amount is Rs 1,000

Key observations from the project

  1. One template method, three order types: processOrder() runs the same 6-step algorithm for transfers, FDs, and loans. The order of steps never changes.
  2. Each processor overrides only its specific steps: FundTransferProcessor validates accounts and IFSC. FixedDepositProcessor validates tenure and calculates interest. LoanApplicationProcessor checks credit score. None of them touch the common steps.
  3. Hook method in action: LoanApplicationProcessor overrides shouldNotify() to return false. The template method checks the hook and skips step 6. Other processors get the default (true) behavior.
  4. Validation failures stop the algorithm: If step 1 throws an exception, steps 2-6 never execute. The template method catches the exception and returns a failure result.
  5. Factory + Template Method combined: OrderProcessorFactory creates the right processor. The client calls processOrder(). One line runs the entire algorithm.
  6. Adding a new order type (e.g., Insurance Premium): Create InsurancePremiumProcessor extends OrderProcessor, implement the 4 abstract methods, add a factory method. No existing code changes.

8. When to Use and When NOT to Use

When to use Template Method

ScenarioExampleWhy Template Method
Fixed algorithm with varying stepsOrder processing, recruitment, data pipelineAlgorithm order is constant, specific steps differ per type
Avoiding code duplicationCommon steps across subclassesWrite once in superclass, inherit everywhere
Enforcing step orderDatabase migration (backup, migrate, verify, cleanup)final template method prevents subclasses from reordering
Framework/library designHttpServlet, JdbcTemplate, AbstractListFramework defines the skeleton, user fills in the specifics
Optional steps neededNotification, logging, cachingHook methods let subclasses opt in/out without changing the algorithm

When NOT to use Template Method

ScenarioWhy NOTUse Instead
All steps are differentNothing is common. Superclass would be empty.Strategy Pattern (swap entire algorithm)
Algorithm order variesTemplate method enforces fixed order. If order must change per subclass, it does not fit.Strategy or Chain of Responsibility
Only one implementationNo subclasses needed. Template method adds unnecessary inheritance.Simple method
Favor composition over inheritanceDeep inheritance hierarchies are hard to maintain.Strategy Pattern (composition-based)

Template Method vs Strategy Pattern

AspectTemplate MethodStrategy
MechanismInheritance (abstract class + subclasses)Composition (interface + implementations)
What variesSome steps of the algorithmThe entire algorithm
Order enforcedYes (final template method)No (each strategy defines its own flow)
Adding variantsNew subclassNew strategy implementation
Runtime switchDifficult (object type is fixed at creation)Easy (swap strategy object at runtime)

9. Quick Reference Card

ComponentRoleIn our project
Abstract classDefines template method + common steps + abstract steps + hooksOrderProcessor
Template methodfinal method that runs the fixed algorithmprocessOrder()
Concrete stepsCommon implementation in superclassgenerateReceipt(), sendNotification()
Abstract stepsSubclass must implementvalidateRequest(), calculateCharges(), etc.
Hook methodDefault implementation, subclass can overrideshouldNotify()
Concrete subclassFills in the specific stepsFundTransferProcessor, FixedDepositProcessor, LoanApplicationProcessor