BLOG POSTS
    MangoHost Blog / Template Method Design Pattern in Java – Example Tutorial
Template Method Design Pattern in Java – Example Tutorial

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 than step1()
  • 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.

Leave a reply

Your email address will not be published. Required fields are marked