Template Method Pattern
1. What is Template Method Pattern?
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:
- Aptitude Test (common for all)
- Group Discussion (common for all)
- Technical Written Test (differs per technology: Java / DotNet / PHP)
- Technical Interview (differs per technology)
- 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
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");
}
}
3. The Solution: Template Method
Create an abstract superclass with:
- Common steps as concrete methods (implemented once)
- Specific steps as abstract methods (subclasses implement)
- Template method marked
finalthat calls all steps in the fixed order
The client calls only the template method. One call runs the entire algorithm.
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");
}
}
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
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 Type | In Superclass | In Subclass | Required? |
|---|---|---|---|
| Concrete method | Fully implemented | Inherited, not overridden | N/A |
| Abstract method | No implementation | Must override | Yes |
| Hook method | Default implementation | Can override | No |
5. Pre-defined Template Method Examples
JDK Template Methods
| Class/Interface | Template Method | Abstract Steps |
|---|---|---|
java.io.InputStream | read(byte[], int, int) | read() (abstract, subclass provides byte reading) |
java.util.AbstractList | addAll(), indexOf() | get(int), size() (abstract) |
java.util.AbstractSet | equals(), removeAll() | iterator(), size() (abstract) |
java.util.AbstractMap | containsKey(), get() | entrySet() (abstract) |
javax.servlet.http.HttpServlet | service() | doGet(), doPost(), doPut(), doDelete() |
Spring Framework Template Classes
| Template Class | Algorithm | What it hides |
|---|---|---|
JdbcTemplate | Get connection, create statement, execute, process results, close | Connection management, resource cleanup, exception translation |
NamedParameterJdbcTemplate | Same as JdbcTemplate with named parameters | Parameter binding by name instead of index |
HibernateTemplate | Open session, begin transaction, execute, commit/rollback, close | Session and transaction management |
RestTemplate | Create HTTP connection, send request, read response, close | HTTP connection management, serialization/deserialization |
// 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.
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
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:
- Validate request (different validation rules per type)
- Check eligibility (different criteria per type)
- Calculate charges (different fee structure per type)
- Execute transaction (different processing logic per type)
- Generate receipt (common for all)
- 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)
Low-Level Design (LLD)
Project Structure
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
Key observations from the project
- One template method, three order types:
processOrder()runs the same 6-step algorithm for transfers, FDs, and loans. The order of steps never changes. - 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.
- 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. - 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.
- Factory + Template Method combined:
OrderProcessorFactorycreates the right processor. The client callsprocessOrder(). One line runs the entire algorithm. - 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
| Scenario | Example | Why Template Method |
|---|---|---|
| Fixed algorithm with varying steps | Order processing, recruitment, data pipeline | Algorithm order is constant, specific steps differ per type |
| Avoiding code duplication | Common steps across subclasses | Write once in superclass, inherit everywhere |
| Enforcing step order | Database migration (backup, migrate, verify, cleanup) | final template method prevents subclasses from reordering |
| Framework/library design | HttpServlet, JdbcTemplate, AbstractList | Framework defines the skeleton, user fills in the specifics |
| Optional steps needed | Notification, logging, caching | Hook methods let subclasses opt in/out without changing the algorithm |
When NOT to use Template Method
| Scenario | Why NOT | Use Instead |
|---|---|---|
| All steps are different | Nothing is common. Superclass would be empty. | Strategy Pattern (swap entire algorithm) |
| Algorithm order varies | Template method enforces fixed order. If order must change per subclass, it does not fit. | Strategy or Chain of Responsibility |
| Only one implementation | No subclasses needed. Template method adds unnecessary inheritance. | Simple method |
| Favor composition over inheritance | Deep inheritance hierarchies are hard to maintain. | Strategy Pattern (composition-based) |
Template Method vs Strategy Pattern
| Aspect | Template Method | Strategy |
|---|---|---|
| Mechanism | Inheritance (abstract class + subclasses) | Composition (interface + implementations) |
| What varies | Some steps of the algorithm | The entire algorithm |
| Order enforced | Yes (final template method) | No (each strategy defines its own flow) |
| Adding variants | New subclass | New strategy implementation |
| Runtime switch | Difficult (object type is fixed at creation) | Easy (swap strategy object at runtime) |
9. Quick Reference Card
| Component | Role | In our project |
|---|---|---|
| Abstract class | Defines template method + common steps + abstract steps + hooks | OrderProcessor |
| Template method | final method that runs the fixed algorithm | processOrder() |
| Concrete steps | Common implementation in superclass | generateReceipt(), sendNotification() |
| Abstract steps | Subclass must implement | validateRequest(), calculateCharges(), etc. |
| Hook method | Default implementation, subclass can override | shouldNotify() |
| Concrete subclass | Fills in the specific steps | FundTransferProcessor, FixedDepositProcessor, LoanApplicationProcessor |
Template Method Design Pattern in Java
1. What is Template Method Pattern?
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:
- Aptitude Test (common for all)
- Group Discussion (common for all)
- Technical Written Test (differs per technology: Java / DotNet / PHP)
- Technical Interview (differs per technology)
- 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
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");
}
}
3. The Solution: Template Method
Create an abstract superclass with:
- Common steps as concrete methods (implemented once)
- Specific steps as abstract methods (subclasses implement)
- Template method marked
finalthat calls all steps in the fixed order
The client calls only the template method. One call runs the entire algorithm.
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");
}
}
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
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 Type | In Superclass | In Subclass | Required? |
|---|---|---|---|
| Concrete method | Fully implemented | Inherited, not overridden | N/A |
| Abstract method | No implementation | Must override | Yes |
| Hook method | Default implementation | Can override | No |
5. Pre-defined Template Method Examples
JDK Template Methods
| Class/Interface | Template Method | Abstract Steps |
|---|---|---|
java.io.InputStream | read(byte[], int, int) | read() (abstract, subclass provides byte reading) |
java.util.AbstractList | addAll(), indexOf() | get(int), size() (abstract) |
java.util.AbstractSet | equals(), removeAll() | iterator(), size() (abstract) |
java.util.AbstractMap | containsKey(), get() | entrySet() (abstract) |
javax.servlet.http.HttpServlet | service() | doGet(), doPost(), doPut(), doDelete() |
Spring Framework Template Classes
| Template Class | Algorithm | What it hides |
|---|---|---|
JdbcTemplate | Get connection, create statement, execute, process results, close | Connection management, resource cleanup, exception translation |
NamedParameterJdbcTemplate | Same as JdbcTemplate with named parameters | Parameter binding by name instead of index |
HibernateTemplate | Open session, begin transaction, execute, commit/rollback, close | Session and transaction management |
RestTemplate | Create HTTP connection, send request, read response, close | HTTP connection management, serialization/deserialization |
// 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.
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
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:
- Validate request (different validation rules per type)
- Check eligibility (different criteria per type)
- Calculate charges (different fee structure per type)
- Execute transaction (different processing logic per type)
- Generate receipt (common for all)
- 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)
Low-Level Design (LLD)
Project Structure
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
Key observations from the project
- One template method, three order types:
processOrder()runs the same 6-step algorithm for transfers, FDs, and loans. The order of steps never changes. - 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.
- 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. - 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.
- Factory + Template Method combined:
OrderProcessorFactorycreates the right processor. The client callsprocessOrder(). One line runs the entire algorithm. - 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
| Scenario | Example | Why Template Method |
|---|---|---|
| Fixed algorithm with varying steps | Order processing, recruitment, data pipeline | Algorithm order is constant, specific steps differ per type |
| Avoiding code duplication | Common steps across subclasses | Write once in superclass, inherit everywhere |
| Enforcing step order | Database migration (backup, migrate, verify, cleanup) | final template method prevents subclasses from reordering |
| Framework/library design | HttpServlet, JdbcTemplate, AbstractList | Framework defines the skeleton, user fills in the specifics |
| Optional steps needed | Notification, logging, caching | Hook methods let subclasses opt in/out without changing the algorithm |
When NOT to use Template Method
| Scenario | Why NOT | Use Instead |
|---|---|---|
| All steps are different | Nothing is common. Superclass would be empty. | Strategy Pattern (swap entire algorithm) |
| Algorithm order varies | Template method enforces fixed order. If order must change per subclass, it does not fit. | Strategy or Chain of Responsibility |
| Only one implementation | No subclasses needed. Template method adds unnecessary inheritance. | Simple method |
| Favor composition over inheritance | Deep inheritance hierarchies are hard to maintain. | Strategy Pattern (composition-based) |
Template Method vs Strategy Pattern
| Aspect | Template Method | Strategy |
|---|---|---|
| Mechanism | Inheritance (abstract class + subclasses) | Composition (interface + implementations) |
| What varies | Some steps of the algorithm | The entire algorithm |
| Order enforced | Yes (final template method) | No (each strategy defines its own flow) |
| Adding variants | New subclass | New strategy implementation |
| Runtime switch | Difficult (object type is fixed at creation) | Easy (swap strategy object at runtime) |
9. Quick Reference Card
| Component | Role | In our project |
|---|---|---|
| Abstract class | Defines template method + common steps + abstract steps + hooks | OrderProcessor |
| Template method | final method that runs the fixed algorithm | processOrder() |
| Concrete steps | Common implementation in superclass | generateReceipt(), sendNotification() |
| Abstract steps | Subclass must implement | validateRequest(), calculateCharges(), etc. |
| Hook method | Default implementation, subclass can override | shouldNotify() |
| Concrete subclass | Fills in the specific steps | FundTransferProcessor, FixedDepositProcessor, LoanApplicationProcessor |