
Template Method Design Pattern in Java – Example Tutorial
The Template Method Design Pattern is one of the most practical behavioral design patterns in Java, allowing you to define the skeleton of an algorithm in a superclass while letting subclasses override specific steps without changing the algorithm’s structure. This pattern is incredibly useful for eliminating code duplication and enforcing consistent workflows across different implementations. You’ll learn how to implement it properly, avoid common pitfalls, and apply it to real-world scenarios like data processing pipelines and web request handling.
How the Template Method Pattern Works
The Template Method pattern operates on a simple but powerful principle: define an algorithm’s structure in an abstract base class, then let concrete subclasses fill in the implementation details. The pattern consists of three main components:
- Abstract Class: Contains the template method and defines abstract methods for steps
- Template Method: A concrete method that calls other methods in a specific sequence
- Hook Methods: Optional methods that subclasses can override for additional customization
The beauty of this pattern lies in the Hollywood Principle – “Don’t call us, we’ll call you.” The framework controls the flow, calling your implementations when needed.
Here’s the basic structure:
public abstract class AbstractClass {
// Template method - defines the algorithm skeleton
public final void templateMethod() {
step1();
step2();
if (hook()) {
step3();
}
step4();
}
protected abstract void step1();
protected abstract void step2();
protected abstract void step4();
// Hook method - optional override
protected boolean hook() {
return true;
}
protected void step3() {
// Default implementation
System.out.println("Default step 3");
}
}
Step-by-Step Implementation Guide
Let’s build a practical example: a data processing pipeline that can handle different file formats. This is something you’d commonly encounter when building VPS-hosted applications that need to process uploaded files.
Step 1: Create the Abstract Base Class
public abstract class DataProcessor {
// Template method - final to prevent overriding
public final ProcessingResult processFile(String filePath) {
System.out.println("Starting file processing: " + filePath);
// Step 1: Validate file
if (!validateFile(filePath)) {
return new ProcessingResult(false, "File validation failed");
}
// Step 2: Read file content
String content = readFile(filePath);
if (content == null) {
return new ProcessingResult(false, "Failed to read file");
}
// Step 3: Process the content (subclass-specific)
Object processedData = processContent(content);
// Step 4: Optional transformation hook
if (shouldTransform()) {
processedData = transformData(processedData);
}
// Step 5: Save results
boolean saved = saveResults(processedData, generateOutputPath(filePath));
System.out.println("Processing completed: " + saved);
return new ProcessingResult(saved, saved ? "Success" : "Save failed");
}
// Abstract methods - must be implemented by subclasses
protected abstract String readFile(String filePath);
protected abstract Object processContent(String content);
protected abstract boolean saveResults(Object data, String outputPath);
// Concrete methods - shared implementation
protected boolean validateFile(String filePath) {
File file = new File(filePath);
return file.exists() && file.canRead() && file.length() > 0;
}
protected String generateOutputPath(String inputPath) {
return inputPath.replaceAll("\\.[^.]+$", "_processed.txt");
}
// Hook methods - optional overrides
protected boolean shouldTransform() {
return false;
}
protected Object transformData(Object data) {
return data;
}
}
Step 2: Create Concrete Implementations
// CSV file processor
public class CsvProcessor extends DataProcessor {
@Override
protected String readFile(String filePath) {
try {
return new String(Files.readAllBytes(Paths.get(filePath)));
} catch (IOException e) {
System.err.println("Error reading CSV file: " + e.getMessage());
return null;
}
}
@Override
protected Object processContent(String content) {
List<String[]> records = new ArrayList<>();
String[] lines = content.split("\n");
for (String line : lines) {
if (!line.trim().isEmpty()) {
records.add(line.split(","));
}
}
System.out.println("Processed " + records.size() + " CSV records");
return records;
}
@Override
protected boolean saveResults(Object data, String outputPath) {
try {
@SuppressWarnings("unchecked")
List<String[]> records = (List<String[]>) data;
StringBuilder result = new StringBuilder();
for (String[] record : records) {
result.append(String.join("|", record)).append("\n");
}
Files.write(Paths.get(outputPath), result.toString().getBytes());
return true;
} catch (IOException e) {
System.err.println("Error saving CSV results: " + e.getMessage());
return false;
}
}
@Override
protected boolean shouldTransform() {
return true; // CSV files need pipe-delimited transformation
}
}
// JSON file processor
public class JsonProcessor extends DataProcessor {
@Override
protected String readFile(String filePath) {
try {
return new String(Files.readAllBytes(Paths.get(filePath)));
} catch (IOException e) {
System.err.println("Error reading JSON file: " + e.getMessage());
return null;
}
}
@Override
protected Object processContent(String content) {
try {
// Simple JSON parsing (in real world, use Jackson or Gson)
Map<String, Object> jsonData = parseSimpleJson(content);
System.out.println("Processed JSON with " + jsonData.size() + " fields");
return jsonData;
} catch (Exception e) {
System.err.println("Error parsing JSON: " + e.getMessage());
return new HashMap<>();
}
}
@Override
protected boolean saveResults(Object data, String outputPath) {
try {
@SuppressWarnings("unchecked")
Map<String, Object> jsonData = (Map<String, Object>) data;
StringBuilder result = new StringBuilder();
for (Map.Entry<String, Object> entry : jsonData.entrySet()) {
result.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
}
Files.write(Paths.get(outputPath), result.toString().getBytes());
return true;
} catch (IOException e) {
System.err.println("Error saving JSON results: " + e.getMessage());
return false;
}
}
private Map<String, Object> parseSimpleJson(String content) {
// Simplified JSON parsing for demo purposes
Map<String, Object> result = new HashMap<>();
// Implementation would use proper JSON library
return result;
}
}
Step 3: Supporting Classes
public class ProcessingResult {
private final boolean success;
private final String message;
public ProcessingResult(boolean success, String message) {
this.success = success;
this.message = message;
}
public boolean isSuccess() { return success; }
public String getMessage() { return message; }
@Override
public String toString() {
return "ProcessingResult{success=" + success + ", message='" + message + "'}";
}
}
Step 4: Usage Example
public class DataProcessingExample {
public static void main(String[] args) {
// Process CSV file
DataProcessor csvProcessor = new CsvProcessor();
ProcessingResult csvResult = csvProcessor.processFile("data.csv");
System.out.println("CSV Result: " + csvResult);
// Process JSON file
DataProcessor jsonProcessor = new JsonProcessor();
ProcessingResult jsonResult = jsonProcessor.processFile("data.json");
System.out.println("JSON Result: " + jsonResult);
// Process files in batch
List<DataProcessor> processors = Arrays.asList(
new CsvProcessor(),
new JsonProcessor()
);
List<String> files = Arrays.asList("file1.csv", "file2.json", "file3.csv");
for (String file : files) {
DataProcessor processor = getProcessorForFile(file, processors);
if (processor != null) {
ProcessingResult result = processor.processFile(file);
System.out.println("Processed " + file + ": " + result);
}
}
}
private static DataProcessor getProcessorForFile(String filename, List<DataProcessor> processors) {
if (filename.endsWith(".csv")) {
return processors.stream()
.filter(p -> p instanceof CsvProcessor)
.findFirst()
.orElse(null);
} else if (filename.endsWith(".json")) {
return processors.stream()
.filter(p -> p instanceof JsonProcessor)
.findFirst()
.orElse(null);
}
return null;
}
}
Real-World Examples and Use Cases
The Template Method pattern shines in several real-world scenarios:
Web Framework Request Processing
public abstract class HttpRequestHandler {
public final HttpResponse handleRequest(HttpRequest request) {
// Authentication
if (!authenticate(request)) {
return new HttpResponse(401, "Unauthorized");
}
// Validation
ValidationResult validation = validateRequest(request);
if (!validation.isValid()) {
return new HttpResponse(400, validation.getError());
}
// Processing
try {
Object result = processRequest(request);
return formatResponse(result);
} catch (Exception e) {
return handleError(e);
}
}
protected boolean authenticate(HttpRequest request) {
// Default implementation
return request.hasHeader("Authorization");
}
protected abstract ValidationResult validateRequest(HttpRequest request);
protected abstract Object processRequest(HttpRequest request);
protected HttpResponse formatResponse(Object result) {
return new HttpResponse(200, result.toString());
}
protected HttpResponse handleError(Exception e) {
return new HttpResponse(500, "Internal Server Error");
}
}
Database Connection Management
This pattern is perfect for managing database operations on dedicated servers where you need consistent connection handling:
public abstract class DatabaseOperation<T> {
public final T execute() {
Connection conn = null;
try {
conn = getConnection();
conn.setAutoCommit(false);
T result = performOperation(conn);
conn.commit();
return result;
} catch (SQLException e) {
rollback(conn);
throw new RuntimeException("Database operation failed", e);
} finally {
closeConnection(conn);
}
}
protected abstract T performOperation(Connection conn) throws SQLException;
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mydb",
"user",
"password"
);
}
private void rollback(Connection conn) {
try {
if (conn != null) conn.rollback();
} catch (SQLException e) {
System.err.println("Rollback failed: " + e.getMessage());
}
}
private void closeConnection(Connection conn) {
try {
if (conn != null) conn.close();
} catch (SQLException e) {
System.err.println("Connection close failed: " + e.getMessage());
}
}
}
// Usage
DatabaseOperation<List<User>> getUsersOperation = new DatabaseOperation<List<User>>() {
@Override
protected List<User> performOperation(Connection conn) throws SQLException {
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE active = ?");
stmt.setBoolean(1, true);
ResultSet rs = stmt.executeQuery();
List<User> users = new ArrayList<>();
while (rs.next()) {
users.add(new User(rs.getInt("id"), rs.getString("name")));
}
return users;
}
};
List<User> users = getUsersOperation.execute();
Game Development AI Behavior
public abstract class AIBehavior {
public final void executeAI(GameContext context) {
// Gather information about environment
EnvironmentData env = analyzeEnvironment(context);
// Make decision based on AI type
AIDecision decision = makeDecision(env, context);
// Execute the decision
if (decision.isValid()) {
executeAction(decision, context);
// Update AI state
updateState(decision, context);
}
// Optional: Learn from the action
if (shouldLearn()) {
learn(decision, context);
}
}
protected EnvironmentData analyzeEnvironment(GameContext context) {
// Common environment analysis
return new EnvironmentData(context.getNearbyEntities(), context.getTerrain());
}
protected abstract AIDecision makeDecision(EnvironmentData env, GameContext context);
protected abstract void executeAction(AIDecision decision, GameContext context);
protected void updateState(AIDecision decision, GameContext context) {
// Default state update
}
protected boolean shouldLearn() {
return false;
}
protected void learn(AIDecision decision, GameContext context) {
// Override for learning AI
}
}
Comparison with Alternative Patterns
Pattern | Use Case | Flexibility | Complexity | Performance |
---|---|---|---|---|
Template Method | Fixed algorithm steps, variable implementation | Medium | Low | High |
Strategy | Interchangeable algorithms | High | Medium | Medium |
Command | Encapsulating requests as objects | High | High | Medium |
Factory Method | Object creation with subclass control | Medium | Low | High |
Template Method vs Strategy Pattern
// Template Method - inheritance-based
abstract class SortingTemplate {
public final void sort(int[] array) {
if (shouldPreprocess()) preprocess(array);
performSort(array); // Abstract method
if (shouldPostprocess()) postprocess(array);
}
protected abstract void performSort(int[] array);
}
// Strategy - composition-based
class SortingContext {
private SortingStrategy strategy;
public void sort(int[] array, SortingStrategy strategy) {
this.strategy = strategy;
strategy.sort(array); // Delegate to strategy
}
}
The Template Method is better when:
- Algorithm structure is fixed and well-defined
- You want to prevent subclasses from changing the workflow
- Common behavior should be centralized
- Performance is critical (no object creation overhead)
Strategy is better when:
- You need to switch algorithms at runtime
- Algorithms are completely independent
- You want to avoid inheritance
Best Practices and Common Pitfalls
Best Practices:
- Make template method final: Prevents subclasses from changing the algorithm structure
- Use meaningful method names:
validateInput()
is better thanstep1()
- Provide hook methods: Allow optional customization without breaking the pattern
- Document the contract: Clearly specify what each abstract method should do
- Handle exceptions properly: Don’t let subclass exceptions break the template flow
public abstract class RobustTemplate {
public final Result executeTemplate(Input input) {
try {
validatePreconditions(input);
Result result = performMainOperation(input);
validatePostconditions(result);
return result;
} catch (PreconditionException e) {
return handlePreconditionFailure(e, input);
} catch (OperationException e) {
return handleOperationFailure(e, input);
} catch (PostconditionException e) {
return handlePostconditionFailure(e, input);
}
}
protected abstract void validatePreconditions(Input input) throws PreconditionException;
protected abstract Result performMainOperation(Input input) throws OperationException;
protected abstract void validatePostconditions(Result result) throws PostconditionException;
// Hook methods with default implementations
protected Result handlePreconditionFailure(PreconditionException e, Input input) {
return Result.failure("Precondition failed: " + e.getMessage());
}
protected Result handleOperationFailure(OperationException e, Input input) {
return Result.failure("Operation failed: " + e.getMessage());
}
protected Result handlePostconditionFailure(PostconditionException e, Input input) {
return Result.failure("Postcondition failed: " + e.getMessage());
}
}
Common Pitfalls to Avoid:
- Too many abstract methods: If you have more than 5-7 abstract methods, consider breaking the pattern down
- Tight coupling: Don’t let subclasses depend on specific implementation details
- Inappropriate inheritance hierarchy: Not every “is-a” relationship needs Template Method
- Missing validation: Always validate inputs and intermediate results
- Poor error handling: Don’t let subclass exceptions propagate uncaught
Anti-pattern Example:
// DON'T DO THIS - too many responsibilities
public abstract class BadTemplate {
public final void doEverything(String input) {
step1(input); // What does this do?
step2(input); // Or this?
step3(input); // Too vague
step4(input); // No clear purpose
step5(input); // Too many steps
step6(input); // Hard to maintain
step7(input); // Violates SRP
}
// Too many abstract methods
protected abstract void step1(String input);
protected abstract void step2(String input);
protected abstract void step3(String input);
protected abstract void step4(String input);
protected abstract void step5(String input);
protected abstract void step6(String input);
protected abstract void step7(String input);
}
Performance Considerations:
The Template Method pattern has excellent performance characteristics:
Aspect | Template Method | Strategy Pattern | Command Pattern |
---|---|---|---|
Method Call Overhead | Virtual method calls | Interface + delegation | Object creation + delegation |
Memory Usage | Single object | Context + Strategy objects | Multiple command objects |
Garbage Collection | Minimal impact | Strategy object lifecycle | Command object creation |
Testing Template Method Classes:
// Create a testable implementation
public class TestableProcessor extends DataProcessor {
private String mockContent;
private boolean simulateReadFailure;
private boolean simulateSaveFailure;
public TestableProcessor(String mockContent) {
this.mockContent = mockContent;
}
public void setSimulateReadFailure(boolean fail) {
this.simulateReadFailure = fail;
}
public void setSimulateSaveFailure(boolean fail) {
this.simulateSaveFailure = fail;
}
@Override
protected String readFile(String filePath) {
return simulateReadFailure ? null : mockContent;
}
@Override
protected Object processContent(String content) {
return content.toUpperCase();
}
@Override
protected boolean saveResults(Object data, String outputPath) {
return !simulateSaveFailure;
}
}
// Test the template method
@Test
public void testSuccessfulProcessing() {
TestableProcessor processor = new TestableProcessor("test content");
ProcessingResult result = processor.processFile("test.txt");
assertTrue(result.isSuccess());
assertEquals("Success", result.getMessage());
}
@Test
public void testReadFailure() {
TestableProcessor processor = new TestableProcessor("test content");
processor.setSimulateReadFailure(true);
ProcessingResult result = processor.processFile("test.txt");
assertFalse(result.isSuccess());
assertEquals("Failed to read file", result.getMessage());
}
The Template Method pattern is particularly powerful for server-side applications where you need consistent processing workflows with customizable steps. Whether you’re building REST APIs, file processors, or database operations, this pattern helps maintain clean, maintainable code while preventing common implementation errors.
For more advanced implementations and integration with frameworks like Spring Boot, check out the official Java documentation on abstract classes and the comprehensive guide on design patterns.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.