You’re building a data processing framework. Every processor reads data, transforms it, and writes output. The read/write steps are always the same. Only the transformation logic varies.

Do you copy the boilerplate to each processor? Or do you define the skeleton once and let each processor fill in the blanks?

What is the Template Method Pattern?

Template Method defines the skeleton of an algorithm in a base class. Subclasses override specific steps without changing the structure. The base class controls the flow; subclasses provide implementations.

Template Method Design Pattern class diagram showing Base class with run template method and abstract steps doA and doB implemented by ImplX and ImplY subclasses

Base.run() calls doA() and doB(). ImplX and ImplY provide their own implementations. The algorithm structure stays fixed.

When to Use Template Method

Use Template Method When Skip Template Method When
Algorithm structure is fixed Algorithm structure varies
Only certain steps need customization Everything might change
You’re building a framework You need runtime flexibility
Inheritance makes sense Composition is better

Template Method is about controlled extension. You decide what can be customized and what stays fixed.

Implementation

Data Processing Framework

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public abstract class DataProcessor {
    
    // Template method - defines the algorithm skeleton
    public final void process() {
        openDataSource();
        
        List<Record> records = readData();
        validateRecords(records);
        
        List<Record> transformed = transformData(records);
        
        writeData(transformed);
        closeDataSource();
        
        // Hook - optional step
        afterProcessing();
    }
    
    // Common implementation - same for all processors
    private void openDataSource() {
        System.out.println("Opening data source connection...");
    }
    
    private void closeDataSource() {
        System.out.println("Closing data source connection...");
    }
    
    private void validateRecords(List<Record> records) {
        System.out.println("Validating " + records.size() + " records...");
        records.removeIf(r -> !r.isValid());
    }
    
    // Abstract methods - subclasses must implement
    protected abstract List<Record> readData();
    protected abstract List<Record> transformData(List<Record> records);
    protected abstract void writeData(List<Record> records);
    
    // Hook method - optional override
    protected void afterProcessing() {
        // Default: do nothing
    }
}

Concrete Implementations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public class CsvProcessor extends DataProcessor {
    private final String inputFile;
    private final String outputFile;
    
    public CsvProcessor(String inputFile, String outputFile) {
        this.inputFile = inputFile;
        this.outputFile = outputFile;
    }
    
    @Override
    protected List<Record> readData() {
        System.out.println("Reading CSV from: " + inputFile);
        return CsvReader.read(inputFile);
    }
    
    @Override
    protected List<Record> transformData(List<Record> records) {
        System.out.println("Transforming CSV data...");
        return records.stream()
            .map(this::normalizeFields)
            .collect(Collectors.toList());
    }
    
    @Override
    protected void writeData(List<Record> records) {
        System.out.println("Writing CSV to: " + outputFile);
        CsvWriter.write(outputFile, records);
    }
    
    private Record normalizeFields(Record record) {
        // CSV-specific transformation
        return record.toUpperCase();
    }
}

public class JsonProcessor extends DataProcessor {
    private final String apiUrl;
    private final String outputPath;
    
    public JsonProcessor(String apiUrl, String outputPath) {
        this.apiUrl = apiUrl;
        this.outputPath = outputPath;
    }
    
    @Override
    protected List<Record> readData() {
        System.out.println("Fetching JSON from API: " + apiUrl);
        return JsonFetcher.fetch(apiUrl);
    }
    
    @Override
    protected List<Record> transformData(List<Record> records) {
        System.out.println("Transforming JSON data...");
        return records.stream()
            .map(this::flattenNested)
            .filter(r -> r.hasRequiredFields())
            .collect(Collectors.toList());
    }
    
    @Override
    protected void writeData(List<Record> records) {
        System.out.println("Writing JSON to: " + outputPath);
        JsonWriter.write(outputPath, records);
    }
    
    @Override
    protected void afterProcessing() {
        // Override hook to add custom behavior
        System.out.println("Sending completion notification...");
        NotificationService.notify("JSON processing complete");
    }
    
    private Record flattenNested(Record record) {
        // JSON-specific transformation
        return record.flatten();
    }
}

public class DatabaseProcessor extends DataProcessor {
    private final String sourceQuery;
    private final String targetTable;
    
    public DatabaseProcessor(String sourceQuery, String targetTable) {
        this.sourceQuery = sourceQuery;
        this.targetTable = targetTable;
    }
    
    @Override
    protected List<Record> readData() {
        System.out.println("Executing query: " + sourceQuery);
        return Database.query(sourceQuery);
    }
    
    @Override
    protected List<Record> transformData(List<Record> records) {
        System.out.println("Transforming database records...");
        return records.stream()
            .map(this::enrichWithMetadata)
            .collect(Collectors.toList());
    }
    
    @Override
    protected void writeData(List<Record> records) {
        System.out.println("Inserting into table: " + targetTable);
        Database.batchInsert(targetTable, records);
    }
    
    private Record enrichWithMetadata(Record record) {
        record.put("processed_at", Instant.now());
        record.put("processor", "DatabaseProcessor");
        return record;
    }
}

Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Process CSV file
DataProcessor csvProcessor = new CsvProcessor("input.csv", "output.csv");
csvProcessor.process();

// Process JSON API
DataProcessor jsonProcessor = new JsonProcessor("https://api.example.com/data", "output.json");
jsonProcessor.process();

// Process database
DataProcessor dbProcessor = new DatabaseProcessor(
    "SELECT * FROM source_table WHERE status = 'pending'",
    "processed_table"
);
dbProcessor.process();

How It Works

sequenceDiagram
    participant Client
    participant AbstractClass as DataProcessor
    participant ConcreteClass as CsvProcessor
    
    Client->>AbstractClass: process()
    AbstractClass->>AbstractClass: openDataSource()
    AbstractClass->>ConcreteClass: readData()
    ConcreteClass-->>AbstractClass: records
    AbstractClass->>AbstractClass: validateRecords()
    AbstractClass->>ConcreteClass: transformData()
    ConcreteClass-->>AbstractClass: transformedRecords
    AbstractClass->>ConcreteClass: writeData()
    AbstractClass->>AbstractClass: closeDataSource()
    AbstractClass->>ConcreteClass: afterProcessing()
    Note over ConcreteClass: Hook (optional override)

The template method orchestrates. Subclasses provide specific implementations.

Hook Methods

Hooks provide optional extension points:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public abstract class Game {
    
    public final void play() {
        initialize();
        
        // Hook - can subclasses have custom start logic?
        if (wantsCustomStart()) {
            customStart();
        } else {
            defaultStart();
        }
        
        while (!isGameOver()) {
            takeTurn();
        }
        
        announceWinner();
        
        // Hook - cleanup is optional
        cleanup();
    }
    
    protected abstract void initialize();
    protected abstract boolean isGameOver();
    protected abstract void takeTurn();
    protected abstract void announceWinner();
    
    // Hook methods with default behavior
    protected boolean wantsCustomStart() {
        return false;  // Default: use standard start
    }
    
    protected void customStart() {
        // Default: nothing
    }
    
    protected void defaultStart() {
        System.out.println("Game started!");
    }
    
    protected void cleanup() {
        // Default: nothing to clean up
    }
}

public class ChessGame extends Game {
    
    @Override
    protected void initialize() {
        setupBoard();
        assignColors();
    }
    
    @Override
    protected boolean wantsCustomStart() {
        return true;  // Override hook
    }
    
    @Override
    protected void customStart() {
        System.out.println("White moves first...");
        startTimer();
    }
    
    @Override
    protected void cleanup() {
        saveGameHistory();
        stopTimer();
    }
    
    // ... other required methods
}

Template Method in Testing

Common pattern for test setup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public abstract class IntegrationTest {
    
    protected Database db;
    protected HttpClient client;
    
    @BeforeEach
    public final void setUp() {
        // Fixed setup sequence
        startTestContainers();
        db = createDatabase();
        migrateSchema();
        client = createHttpClient();
        
        // Hook for subclass-specific setup
        beforeEach();
    }
    
    @AfterEach
    public final void tearDown() {
        // Hook for subclass-specific cleanup
        afterEach();
        
        // Fixed cleanup sequence
        cleanDatabase();
        stopTestContainers();
    }
    
    // Hooks for subclasses
    protected void beforeEach() { }
    protected void afterEach() { }
    
    // Subclasses can override for custom database
    protected Database createDatabase() {
        return new PostgresDatabase();
    }
}

public class UserServiceTest extends IntegrationTest {
    
    private UserService userService;
    
    @Override
    protected void beforeEach() {
        userService = new UserService(db);
        seedTestUsers();
    }
    
    @Override
    protected void afterEach() {
        // Custom cleanup
        userService.clearCache();
    }
    
    @Test
    void shouldFindUserById() {
        User user = userService.findById("test-user-1");
        assertNotNull(user);
    }
}

Template Method vs Strategy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// TEMPLATE METHOD - inheritance
public abstract class OrderProcessor {
    public final void process(Order order) {
        validate(order);
        calculateTotal(order);  // Subclass implements
        applyDiscounts(order);  // Subclass implements
        charge(order);
        sendConfirmation(order);
    }
    
    protected abstract void calculateTotal(Order order);
    protected abstract void applyDiscounts(Order order);
}

// STRATEGY - composition
public class OrderProcessor {
    private final PricingStrategy pricing;
    private final DiscountStrategy discounts;
    
    public OrderProcessor(PricingStrategy pricing, DiscountStrategy discounts) {
        this.pricing = pricing;
        this.discounts = discounts;
    }
    
    public void process(Order order) {
        validate(order);
        pricing.calculate(order);      // Injected strategy
        discounts.apply(order);        // Injected strategy
        charge(order);
        sendConfirmation(order);
    }
}

Template Method is simpler but less flexible. Strategy allows runtime changes and better testing.

Common Mistakes

1. Making Template Method Non-Final

Subclasses shouldn’t override the template method:

1
2
3
4
5
// Wrong - subclass can break the algorithm
public void process() { ... }

// Right - subclass cannot change the skeleton
public final void process() { ... }

2. Too Many Abstract Methods

Keep the extension points minimal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Too many - subclass must implement everything
public abstract class Processor {
    protected abstract void step1();
    protected abstract void step2();
    protected abstract void step3();
    protected abstract void step4();
    protected abstract void step5();
    // Subclass is basically writing everything
}

// Better - fewer customization points with sensible defaults
public abstract class Processor {
    protected abstract void transform(Data data);  // One required
    protected void beforeTransform() { }           // Optional hook
    protected void afterTransform() { }            // Optional hook
}

3. Calling Super Methods Wrong

Make the contract clear:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Confusing - must caller call super?
public class Child extends Parent {
    @Override
    protected void doWork() {
        super.doWork();  // Required? Optional?
        // Child-specific work
    }
}

// Clear - template method handles sequence
public abstract class Parent {
    public final void work() {
        beforeWork();   // Parent's code
        doWork();       // Child implements
        afterWork();    // Parent's code
    }
    
    protected abstract void doWork();
}

Real-World Examples

Java I/O Streams: InputStream.read(byte[]) uses template method that calls read().

JUnit: @BeforeEach, @Test, @AfterEach lifecycle is a template method.

Servlet: HttpServlet.service() calls doGet(), doPost(), etc.

Spring: AbstractController, JdbcTemplate, and many framework classes.

Strategy is the composition-based alternative. More flexible, less coupled.

Factory Method is often used within template methods to create objects.

Hook methods are a form of the Hollywood Principle: “Don’t call us, we’ll call you.”

Wrapping Up

Template Method defines algorithm structure in a base class, letting subclasses customize specific steps. It’s perfect for frameworks where you want controlled extension.

Use final for the template method. Provide hooks for optional customization. Keep abstract methods minimal.

When you need runtime flexibility or better testability, consider Strategy instead. Template Method trades flexibility for simplicity.


Further Reading: