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.
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. The abstract methods are the required extension points; the hook method (shouldSendNotification()) is optional.
package template;
/**
* Abstract class — defines the template method and extension points.
*
* Primitive operations (abstract): MUST be implemented by subclasses.
* Hook operations (non-abstract): CAN be overridden by subclasses.
*/
public abstract class DataMigration {
// ── Template method ──────────────────────────────────────────────────────
// final = subclasses cannot change the sequence
public final void migrate() {
System.out.println("▶ Starting migration from: " + getSourceName());
connect();
List<String> rawData = readData();
System.out.println(" Read " + rawData.size() + " records");
List<String> transformed = transformData(rawData);
System.out.println(" Transformed to " + transformed.size() + " records");
writeData(transformed);
disconnect();
if (shouldSendNotification()) { // hook — subclass can opt out
sendNotification("Migration from " + getSourceName() + " complete.");
}
System.out.println("✔ Migration done\n");
}
// ── Primitive operations (required) ──────────────────────────────────────
protected abstract String getSourceName();
protected abstract void connect();
protected abstract List<String> readData();
protected abstract List<String> transformData(List<String> data);
// ── Concrete step shared by all subclasses ────────────────────────────────
private void writeData(List<String> data) {
System.out.println(" Writing " + data.size() + " records to target database");
}
private void disconnect() {
System.out.println(" Disconnecting from " + getSourceName());
}
private void sendNotification(String message) {
System.out.println(" 📧 Notification sent: " + message);
}
// ── Hook (optional override) ──────────────────────────────────────────────
protected boolean shouldSendNotification() { return true; }
}
package template;
import java.util.List;
import java.util.stream.Collectors;
/** Concrete implementation — reads from a CSV file */
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 file: " + filePath);
}
@Override
protected List<String> readData() {
// Simulate reading CSV rows
return List.of("Alice,30,Engineer", "Bob,25,Designer", "Carol,35,Manager");
}
@Override
protected List<String> transformData(List<String> data) {
// Parse CSV and reformat as JSON-like strings for the target
return data.stream()
.map(row -> {
String[] parts = row.split(",");
return "{ name: " + parts[0] + ", age: " + parts[1] + ", role: " + parts[2] + " }";
})
.collect(Collectors.toList());
}
// Uses default shouldSendNotification() -> true
}
package template;
import java.util.List;
/** Concrete implementation — fetches from a REST API; opts out of notifications */
public class ApiMigration extends DataMigration {
private final String apiUrl;
public ApiMigration(String apiUrl) { this.apiUrl = apiUrl; }
@Override protected String getSourceName() { return "API(" + apiUrl + ")"; }
@Override
protected void connect() {
System.out.println(" Authenticating with API: " + apiUrl);
}
@Override
protected List<String> readData() {
// Simulate paginated API fetch
return List.of(
"{\"id\":1,\"user\":\"alice\",\"score\":95}",
"{\"id\":2,\"user\":\"bob\",\"score\":87}"
);
}
@Override
protected List<String> transformData(List<String> data) {
// Remap API field names to target schema
return data.stream()
.map(json -> json.replace("\"user\"", "\"username\"").replace("\"score\"", "\"points\""))
.toList();
}
// Hook override — API migrations are silent (caller handles alerting)
@Override
protected boolean shouldSendNotification() { return false; }
}
package template;
public class Main {
public static void main(String[] args) {
System.out.println("=== Template Method Design Pattern Demo ===\n");
DataMigration csvMigration = new CsvMigration("users_export.csv");
csvMigration.migrate();
DataMigration apiMigration = new ApiMigration("https://api.example.com/users");
apiMigration.migrate();
}
}
Console Output
▶ Starting migration from: CSV(users_export.csv)
Opening file: users_export.csv
Read 3 records
Transformed to 3 records
Writing 3 records to target database
Disconnecting from CSV(users_export.csv)
📧 Notification sent: Migration from CSV(users_export.csv) complete.
✔ Migration done
▶ Starting migration from: API(https://api.example.com/users)
Authenticating with API: https://api.example.com/users
Read 2 records
Transformed to 2 records
Writing 2 records to target database
Disconnecting from API(https://api.example.com/users)
(notification suppressed — hook returned false)
✔ Migration done
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