Every data migration your team runs follows the same high-level script: connect to the source, read the data, transform it for the target, write it, disconnect. The script never changes. What changes is how each step is carried out — a CSV migration reads files, an API migration makes HTTP calls. The Template Method pattern puts the invariant sequence in one place (an abstract base class) and delegates the varying steps to subclasses. You can’t accidentally reorder the steps, and you can’t forget a step — the skeleton enforces the contract.
This pattern is sometimes dismissed as “just inheritance” — and when overused it can lead to deep, fragile hierarchies. Used well, it’s one of the cleanest ways to express a fixed process with variable implementations. The JDK uses it in HttpServlet, AbstractList, and InputStream.
All code compiles and runs with Java 25. No external dependencies required.
Pattern Structure

final to prevent override. Diagram: refactoring.guruImplementation: Data Migration Pipeline
The abstract base class owns the migrate() method. It is final — no subclass can reorder, skip, or add steps. getSourceName(), connect(), readData(), and transformData() are the required extension points; validate(), writeData(), and disconnect() are concrete steps shared by every subclass; sendNotification() is an optional hook a subclass can override to opt out.
DataMigration.java
package template;
/**
* Abstract Class with Template Method.
* The overall migration algorithm is fixed here; subclasses fill in
* the source-specific steps (connect, read, transform).
*/
public abstract class DataMigration {
// THE TEMPLATE METHOD — defines the fixed algorithm skeleton
public final void migrate() {
System.out.println("\n[" + getSourceName() + "] Starting migration...");
connect();
validate();
int rowCount = readData();
int transformed = transformData(rowCount);
writeData(transformed);
if (sendNotification()) {
notifyStakeholders();
}
disconnect();
System.out.println("[" + getSourceName() + "] Migration complete.\n");
}
// Abstract steps — must be implemented by each subclass
protected abstract String getSourceName();
protected abstract void connect();
protected abstract int readData();
protected abstract int transformData(int rawCount);
// Concrete steps — common to all migrations
protected void validate() {
System.out.println(" Validating schema compatibility...");
}
protected void writeData(int count) {
System.out.println(" Writing " + count + " rows to target...");
}
protected void disconnect() {
System.out.println(" Closing source connection.");
}
// Hook — subclasses may override to change behaviour
protected boolean sendNotification() { return true; }
protected void notifyStakeholders() {
System.out.println(" Sending completion email to team.");
}
}
CsvMigration implements the four required steps for a CSV source. It uses every concrete and hook default unchanged:
CsvMigration.java
package template;
public class CsvMigration extends DataMigration {
private final String filePath;
public CsvMigration(String filePath) { this.filePath = filePath; }
@Override protected String getSourceName() { return "CSV:" + filePath; }
@Override
protected void connect() {
System.out.println(" Opening CSV file: " + filePath);
}
@Override
protected int readData() {
System.out.println(" Parsing CSV rows...");
return 1_500; // simulated row count
}
@Override
protected int transformData(int rawCount) {
System.out.println(" Mapping CSV columns to target schema (" + rawCount + " rows)...");
return rawCount; // no rows lost
}
}
ApiMigration implements the same four steps for a REST source, and additionally overrides the sendNotification() hook so API migrations stay silent — the caller handles alerting on its own:
ApiMigration.java
package template;
public class ApiMigration extends DataMigration {
private final String endpoint;
public ApiMigration(String endpoint) { this.endpoint = endpoint; }
@Override protected String getSourceName() { return "API:" + endpoint; }
@Override
protected void connect() {
System.out.println(" Authenticating with API at " + endpoint + "...");
}
@Override
protected int readData() {
System.out.println(" Paginating through API responses...");
return 320;
}
@Override
protected int transformData(int rawCount) {
System.out.println(" Flattening JSON, deduplicating (" + rawCount + " records)...");
return rawCount - 15; // 15 duplicates removed
}
// Override hook: silent migrations from APIs, no email spam
@Override
protected boolean sendNotification() { return false; }
}
Main runs both migrations through the identical migrate() skeleton and prints what stayed the same versus what each subclass varied:
Main.java
package template;
/**
* Template Method Design Pattern — Runnable Demo
* Run: javac template/*.java -d out/template && java -cp out/template template.Main
* Article: https://ankurm.com/template-method-design-pattern-java/
*/
public class Main {
public static void main(String[] args) {
System.out.println("=== Template Method Design Pattern Demo ===");
DataMigration csvJob = new CsvMigration("customers_export.csv");
csvJob.migrate();
DataMigration apiJob = new ApiMigration("https://api.partner.com/v2/orders");
apiJob.migrate();
System.out.println("-- Both jobs used the same migrate() skeleton --");
System.out.println("-- Each provided its own connect/read/transform implementation --");
System.out.println("-- ApiMigration overrode the sendNotification() hook: no email --");
System.out.println("\n=== Demo complete ===");
}
}
Console Output
[CSV:customers_export.csv] Starting migration…
Opening CSV file: customers_export.csv
Validating schema compatibility…
Parsing CSV rows…
Mapping CSV columns to target schema (1500 rows)…
Writing 1500 rows to target…
Sending completion email to team.
Closing source connection.
[CSV:customers_export.csv] Migration complete.
[API:https://api.partner.com/v2/orders] Starting migration…
Authenticating with API at https://api.partner.com/v2/orders…
Validating schema compatibility…
Paginating through API responses…
Flattening JSON, deduplicating (320 records)…
Writing 305 rows to target…
Closing source connection.
[API:https://api.partner.com/v2/orders] Migration complete.
— Both jobs used the same migrate() skeleton —
— Each provided its own connect/read/transform implementation —
— ApiMigration overrode the sendNotification() hook: no email —
=== Demo complete ===
The Hollywood Principle
Template Method is the textbook example of the Hollywood Principle: “Don’t call us, we’ll call you.” The abstract class calls the subclass methods — subclasses never call the template method directly. Control lives in the base class. This inversion of control is the defining feature of frameworks: Spring calls your @Service methods; you don’t call Spring. JUnit calls your @Test methods; you don’t call JUnit. Both are Template Method in disguise.
💡 Abstract Methods vs. Hooks: Abstract methods are mandatory extension points — subclasses must provide them or the class won’t compile. Hook methods have a default implementation and are optional — subclasses may override them for customisation. A good rule of thumb: if every subclass will need to provide a different implementation, make it abstract. If most subclasses will use the default and only some will customise it, make it a hook. Too many abstract methods makes the class painful to subclass; too many hooks makes the template hard to reason about.
Template Method in the JDK
javax.servlet.HttpServlet — service() is the template method; doGet(), doPost() etc. are the primitive operations you override. java.io.InputStream — read(byte[], int, int) is implemented in terms of the abstract single-byte read(). java.util.AbstractList — iterator(), listIterator(), and subList() are all defined in terms of abstract get(int) and size(). junit.framework.TestCase — runBare() calls setUp(), then your test method, then tearDown() — classic template method.
Template Method vs. Strategy
Both patterns address algorithm variability, but at different granularities and with different mechanisms. Template Method uses inheritance: the algorithm skeleton is fixed in the parent class, and subclasses vary the steps. The relationship is established at compile-time. Strategy uses composition: the algorithm itself is swapped at runtime by injecting a different object. Template Method is appropriate when variations share a fixed structure and the overhead of a separate class per variant is unjustified. Strategy is appropriate when you need runtime switching, when combining multiple algorithms, or when the algorithms don’t share a structural skeleton.
⚠️ Inheritance Pitfalls: Template Method is one of the few patterns that mandates inheritance, and it comes with the usual risks. Subclasses are tightly coupled to the base class — a change to the template method breaks every subclass. Deep hierarchies become hard to reason about. The Liskov Substitution Principle is easy to violate if abstract method contracts aren’t documented clearly. Prefer the pattern for shallow hierarchies (one level of concrete subclasses) and document the invariants each abstract method must uphold. If you find yourself needing three or more levels of Template Method inheritance, consider switching to Strategy with composition.
Runnable Code on GitHub
Full source at ankurm.com/git.app/asmhatre/design-patterns under 03-behavioral/template-method/.
javac 03-behavioral/template-method/*.java -d out/template
java -cp out/template template.Main
See Also
- Strategy Design Pattern in Java — swaps the whole algorithm at runtime via composition; use when you need runtime switching
- Factory Method Design Pattern in Java — a specialised Template Method where the varying step is object creation
- Visitor Design Pattern in Java — adds operations to a class hierarchy without modifying it; Template Method fixes the class hierarchy and varies operations per subclass
Further Reading
- Template Method — Refactoring.Guru
- Design Patterns: Elements of Reusable Object-Oriented Software — Gamma et al., Chapter 5
No Comments yet!