Java design pattern deep dive
Template Method Pattern in Java: 100 interview questions with professional answers.
Learn how the Template Method pattern locks down the shape of an algorithm in a final base-class method while letting subclasses override only the steps that legitimately vary, why real frameworks from JUnit to Spring's JdbcTemplate lean on this idea, and how to spot when inheritance-based variation is the right call versus composition-based Strategy.
What makes a good Template Method answer?
Interviewers want to see that you understand this as a discipline about which parts of an algorithm are allowed to move, not just "an abstract class with some abstract methods."
final so subclasses cannot reorder or skip steps.| Approach | Use when | Watch out for |
|---|---|---|
| Template Method (inheritance) | Several classes share a deep, tightly-coupled fixed skeleton and only a couple of steps genuinely vary per subclass. | Fragile base class problem; a change to the skeleton or a new hook can ripple across every subclass. |
| Strategy (composition) | You need to swap the varying behavior at runtime, share it across unrelated class hierarchies, or unit test it in isolation. | One more object to wire up per variation; can feel like overkill for a single, permanent variation point. |
| Plain callback/lambda parameter | There is exactly one concrete method with exactly one varying step, and a functional interface parameter reads more simply than a class hierarchy. | Scales poorly once the "fixed" part around the callback grows into several coordinated steps. |
Topics
Interview questions and answers
Each answer gives the implementation direction, the trade-off to mention, and the production concern that makes the answer stronger.
1. Explain the Template Method design pattern in Java and the general problem it solves for algorithms that share structure but differ in specific steps.
Template Method defines the skeleton of an algorithm in a base class method, deferring some of its steps to subclasses. It solves the problem of several related algorithms sharing the same overall sequence, but each needing to perform one or two steps differently, without duplicating the shared parts.
The base class owns the invariant parts: the order of operations and any shared setup or cleanup. Subclasses own only the parts that legitimately vary, which keeps the shared logic in exactly one place and makes each variant trivially easy to compare against the others.
abstract class DataPipeline {
public final void run() {
Object raw = readData();
Object parsed = parseData(raw);
if (validateData(parsed)) {
saveResult(parsed);
}
}
protected abstract Object parseData(Object raw);
protected boolean validateData(Object parsed) { return true; } // hook, default no-op
private Object readData() { /* fixed I/O */ return null; }
private void saveResult(Object parsed) { /* fixed persistence */ }
}
2. Describe the GoF roles in the Template Method pattern: AbstractClass, primitive operations, hooks, and ConcreteClass, and how they relate to each other structurally.
AbstractClass declares the template method, usually final, plus a set of abstract primitive operations and optional hook methods with default bodies. ConcreteClass extends AbstractClass and overrides the primitive operations it must supply, and any hooks whose default behavior does not fit its case.
The template method itself is never overridden; it is the one piece of behavior guaranteed to be identical across every ConcreteClass. Everything a ConcreteClass controls is expressed purely through which of the abstract methods and hooks it chooses to override, never by touching the orchestration logic.
abstract class AbstractClass {
public final void templateMethod() {
stepOne();
stepTwo(); // abstract primitive operation
if (hook()) { // optional hook, default true
stepThree();
}
}
private void stepOne() { }
protected abstract void stepTwo();
protected boolean hook() { return true; }
private void stepThree() { }
}
3. Explain the Hollywood Principle ("don't call us, we'll call you") and how the Template Method pattern embodies it through inversion of control at the method level.
The Hollywood Principle says lower-level or subclass code should not actively call into higher-level, framework-owned code; instead, the higher-level code calls into the subclass at well-defined points it controls. In Template Method, the AbstractClass is "Hollywood": it calls parseData(), validateData(), and so on, at the exact moments its fixed algorithm decides, and the subclass never calls the template method itself or dictates when its own overridden steps run.
This inversion is what keeps the overall control flow centralized and auditable in one place, the base class, rather than scattered across every subclass that might otherwise decide independently when and how often to invoke shared logic.
4. Why is the template method itself typically declared final in the abstract class, and what bugs does this prevent?
Marking the template method final guarantees the fixed sequence of steps can never be reordered, partially skipped, or replaced by an overriding subclass, which is the entire point of the pattern: only the designated steps vary, not the orchestration. Without final, nothing stops a subclass from overriding the template method wholesale and calling its steps in a different order, or omitting the validation step entirely, silently breaking every invariant the base algorithm was written to guarantee.
abstract class ReportGenerator {
public final void generate() { // final: order can never change
fetchData();
renderHeader();
renderBody();
renderFooter();
}
protected abstract void renderBody();
private void fetchData() { }
private void renderHeader() { }
private void renderFooter() { }
}
generate() directly could call renderBody() before fetchData(), producing a report body built from stale or absent data with no compiler warning.5. Walk through a worked data-processing pipeline example with readData() fixed, parseData() abstract, validateData() a hook with a default, and saveResult() fixed.
The pipeline's shape never changes: read, parse, optionally validate, then save. readData() and saveResult() are private or final fixed steps because every subclass reads from the same configured source and saves through the same persistence layer. parseData() is abstract because CSV, JSON, and fixed-width parsers genuinely have nothing in common to default to. validateData() is a hook because most formats need no extra validation beyond what parsing itself already enforces, but a subclass handling untrusted input can override it to add checks.
class CsvDataPipeline extends DataPipeline {
@Override
protected Object parseData(Object raw) { return CsvParser.parse((String) raw); }
@Override
protected boolean validateData(Object parsed) {
return ((List<?>) parsed).size() > 0; // stricter than the default no-op hook
}
}
class JsonDataPipeline extends DataPipeline {
@Override
protected Object parseData(Object raw) { return JsonParser.parse((String) raw); }
// validateData left at the default hook: no extra check needed
}
6. What is the difference between an abstract "primitive operation" that a subclass must override and a "hook" method that a subclass may optionally override?
A primitive operation is declared abstract because the AbstractClass has no reasonable default for it; every ConcreteClass must supply its own implementation or the code will not compile. A hook is a concrete method with a sensible default body, often a no-op or a fixed return value, that a subclass may override to customize behavior but is never required to.
This is a design-judgment call, not a syntax rule: choosing abstract signals "you must decide this," while choosing a hook with a default signals "most subclasses are fine with this, but you may override it." Getting the choice wrong in either direction creates friction, either forcing needless boilerplate overrides or letting subclasses silently miss a step they should have customized.
7. Give a concrete example where using an abstract primitive operation is the wrong choice and a hook with a sensible default would be better design.
Suppose an OrderProcessor template declares protected abstract void applyDiscount(Order order). Forcing every single ConcreteClass, including a plain standard-priced order type, to override this method just to write an empty body is needless ceremony. Making it a hook with a default no-op body means only the promotional or loyalty-tier subclasses that actually need to change the price have to override anything at all.
abstract class OrderProcessor {
public final void process(Order order) {
validate(order);
applyDiscount(order); // hook, not abstract
charge(order);
}
protected void applyDiscount(Order order) { /* default: no discount */ }
protected abstract void validate(Order order);
private void charge(Order order) { }
}
8. Explain how java.io.InputStream's read() methods and OutputStream's write() methods illustrate the Template Method pattern in the JDK.
InputStream declares read() as abstract, reading a single byte, but implements read(byte[]) and read(byte[], int, int) as concrete methods built entirely in terms of the single-byte read(), looping and handling end-of-stream. A subclass only has to implement the one abstract read() method to get fully working bulk-read behavior for free.
public abstract class InputStream implements Closeable {
public abstract int read() throws IOException; // primitive operation
public int read(byte[] b) throws IOException { // fixed, built on read()
return read(b, 0, b.length);
}
// read(byte[], int, int) loops calling read() internally
}
OutputStream.write(byte[]) works the same way, ultimately looping over the abstract single-byte write(int) unless a subclass overrides the bulk method for efficiency, which many concrete subclasses do purely as a performance optimization rather than a correctness requirement.
9. Explain how java.util.AbstractList's get() and size() abstract methods let a subclass gain a fully functional List by overriding only two methods.
AbstractList implements iterator(), indexOf(), contains(), toString(), and more, entirely in terms of the two abstract methods get(int index) and size(). A subclass that implements only those two methods, such as a list backed by a fixed-size array, inherits dozens of fully correct List operations for free.
class ImmutableArrayList<T> extends AbstractList<T> {
private final Object[] data;
ImmutableArrayList(Object[] data) { this.data = data; }
@Override public T get(int index) { return (T) data[index]; }
@Override public int size() { return data.length; }
// iterator(), contains(), indexOf(), toString() all inherited, all correct
}
10. How does AbstractMap use the Template Method pattern via entrySet() to provide default implementations of get(), containsKey(), and other Map methods?
AbstractMap declares entrySet() as abstract and implements get(Object key), containsKey(Object key), size(), and toString() by iterating over whatever Set<Entry<K,V>> the subclass's entrySet() returns. A subclass with an unusual internal storage layout only has to expose that storage as an entry set to inherit a large, correct Map implementation.
This is the same trade as AbstractList: correctness and consistency across every subclass in exchange for possibly suboptimal performance, since the default get() built on entrySet() iteration is O(n) unless the subclass overrides it with something faster, such as a real hash lookup.
11. Explain how JUnit's test lifecycle (@BeforeEach, test method, @AfterEach) embodies the Template Method concept, even though JUnit doesn't use direct inheritance to achieve it.
Conceptually, JUnit runs a fixed sequence for every test: run any @BeforeEach methods, run the test method, then run any @AfterEach methods, guaranteeing teardown even if the test fails. That fixed sequence is exactly the "template method," and your test class's individual @BeforeEach/test/@AfterEach methods are the "steps" whose bodies you supply.
The difference from classic Template Method is mechanism, not concept: JUnit 5 discovers and invokes these methods reflectively through annotations and an extension model, rather than requiring your test class to extend a common base class and override named abstract methods, which is precisely why it scales to multiple independent extensions instead of one rigid inheritance chain.
12. Compare JUnit 3's TestCase class-based template method lifecycle (setUp/tearDown via inheritance) to JUnit 5's annotation-based extension model, and explain why the design moved away from inheritance.
JUnit 3's TestCase was a textbook Template Method: it declared a fixed runBare() that called setUp(), then the test method (found by name convention), then tearDown(), and your test class extended TestCase overriding setUp()/tearDown(). This worked, but Java's single inheritance meant a test class could not also extend some other useful base class, and combining multiple cross-cutting lifecycle concerns, such as a database rule and a temp-folder rule, required awkward multiple inheritance workarounds.
JUnit 5 replaced the base-class hierarchy with composable extensions and annotations, so a test class can layer any number of independent lifecycle behaviors without touching a single shared superclass, trading Template Method's simplicity for Strategy-like composability at the framework level.
13. Explain how HttpServlet.service() dispatches to doGet(), doPost(), and other HTTP-method-specific handlers as a real-world Template Method example.
HttpServlet.service(HttpServletRequest, HttpServletResponse) is the fixed template step: it inspects the incoming HTTP method and dispatches to the matching protected method, doGet(), doPost(), doPut(), doDelete(), and so on. Each of those is effectively a hook with a default implementation that returns HTTP 405 Method Not Allowed, and a servlet subclass overrides only the methods it actually supports.
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// handle GET only; doPost/doPut keep their default 405 behavior
}
}
14. Discuss whether overriding service() directly in a servlet subclass, bypassing doGet()/doPost(), breaks the Template Method contract, and why frameworks discourage it.
Technically service() is not marked final on HttpServlet, so it can be overridden, but doing so bypasses the entire dispatch mechanism the pattern was built to provide: correct method-not-allowed handling, HEAD-request special casing built on top of doGet(), and any consistent cross-cutting behavior other code layers assume runs through service().
Frameworks discourage this because it defeats the purpose of the template: a servlet that overrides service() directly must reimplement everything the base class already handled correctly, and it silently stops benefiting from any future improvements the servlet container makes to the shared dispatch logic.
15. Explain the naming convention behind Spring's JdbcTemplate, JmsTemplate, and RestTemplate classes and how they apply "fixed skeleton, you supply the callback" thinking.
Each "Template" class fixes the tedious, error-prone, boilerplate-heavy parts of an operation: acquiring a connection, opening a statement, handling resource cleanup in a finally block, translating vendor-specific exceptions into a consistent hierarchy, and committing or closing correctly even when an exception is thrown. The one part left open is the actual business logic: which SQL to run and how to map a row, which message to send, which URL and body to post.
List<User> users = jdbcTemplate.query(
"SELECT id, name FROM users WHERE active = ?",
ps -> ps.setBoolean(1, true), // caller supplies the "step"
(rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name")));
The naming signals exactly the Template Method idea even though the mechanism is a callback object rather than subclassing: the "template" is fixed, the caller only supplies the varying step.
16. Is Spring's JdbcTemplate really an implementation of the GoF Template Method pattern, or is it closer to Strategy because it uses callback objects instead of subclassing? Make the case for both sides.
The case for Template Method: the intent matches exactly, a fixed skeleton with one designated varying step, and the naming and mental model deliberately echo the pattern. The case for Strategy: structurally, JdbcTemplate takes a RowMapper or PreparedStatementSetter object as a constructor or method parameter, composition rather than inheritance, which is the textbook mechanism of Strategy, not Template Method.
The honest answer in an interview is that JdbcTemplate achieves a Template-Method-shaped intent through a Strategy-shaped mechanism: it is "Template Method thinking, implemented with Strategy's tools," which is exactly why modern Java increasingly favors composition even when the underlying design problem is classically framed as Template Method.
17. Walk through implementing a JdbcTemplate-style class yourself: fix connection acquisition, statement execution, and resource cleanup, while a caller supplies only the row-mapping logic.
The fixed skeleton opens a connection, prepares the statement, executes the query, and guarantees the ResultSet, statement, and connection are all closed in finally blocks (or via try-with-resources) regardless of whether row mapping throws. The only thing a caller supplies is a RowMapper functional interface invoked once per row.
class MiniJdbcTemplate {
private final DataSource dataSource;
MiniJdbcTemplate(DataSource dataSource) { this.dataSource = dataSource; }
<T> List<T> query(String sql, RowMapper<T> mapper) throws SQLException {
List<T> results = new ArrayList<>();
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
int rowNum = 0;
while (rs.next()) {
results.add(mapper.mapRow(rs, rowNum++));
}
}
return results;
}
}
interface RowMapper<T> { T mapRow(ResultSet rs, int rowNum) throws SQLException; }
18. Compare Template Method to Strategy: both vary part of an algorithm, but one uses inheritance and one uses composition. Explain the essential structural difference with code.
Template Method varies behavior by subclassing: the varying step is a method the subclass overrides, and the algorithm and the variation live in the same object, related by inheritance. Strategy varies behavior by composition: the varying step is delegated to a separate object implementing a small interface, injected at construction or call time, and the algorithm and the variation live in two different objects, related by delegation.
// Template Method: variation via subclassing
abstract class Sorter { public final void sort(int[] a) { /* fixed loop */ compare(a, 0, 1); } protected abstract int compare(int[] a, int i, int j); }
// Strategy: variation via composition
class Sorter2 {
private final Comparator<Integer> comparator;
Sorter2(Comparator<Integer> comparator) { this.comparator = comparator; }
public void sort(int[] a) { /* fixed loop */ comparator.compare(a[0], a[1]); }
}
19. Under what circumstances would you actually prefer Template Method's inheritance-based approach over Strategy's composition-based approach in modern Java?
Prefer Template Method when the fixed and variable parts are deeply, structurally coupled, sharing internal state or intermediate computed values that would otherwise have to be threaded awkwardly through a Strategy interface's parameters. It also fits when the set of variants is small, closed, and known at compile time, and when you genuinely never need to swap the variation at runtime for the same instance.
It also remains a reasonable choice when the "steps" naturally form a family with an established, stable base class already in place, such as extending an existing JDK abstract class like AbstractList, where introducing a Strategy object purely for architectural purity would add indirection without a corresponding benefit.
20. Why is composition generally preferred over inheritance in modern software design, and how does this bias apply specifically to choosing between Template Method and Strategy?
Composition is preferred because it avoids the fragile base class problem, keeps variation points swappable at runtime, allows one variation object to be reused across unrelated class hierarchies, and is trivially mockable in unit tests without needing a throwaway subclass. Inheritance, by contrast, locks the relationship at compile time, exposes protected internals to every subclass, and makes it easy for a base class change to silently break subclasses far away in the codebase.
Applied to this pattern specifically: default to Strategy unless the coupling between fixed and variable steps is genuinely deep enough that composition would just relocate the complexity rather than remove it, in which case Template Method's tighter coupling is honestly reflecting the real design, not a shortcut.
21. Explain how Factory Method is very often used as one of the "hook" steps inside a Template Method, using GoF's own framework example.
GoF's own illustrative example pairs the two patterns directly: an application framework's document-handling template method calls createDocument() at the point where it needs a new document instance, and createDocument() is itself a Factory Method, an abstract or overridable method whose entire job is to instantiate the correct product subclass. The template method never needs to know which concrete document class gets created; it just calls the factory method hook at the right moment in its fixed sequence.
abstract class Application {
public final void newDocument() {
Document doc = createDocument(); // Factory Method, called as a template step
doc.open();
addToRecentFiles(doc);
}
protected abstract Document createDocument();
private void addToRecentFiles(Document doc) { }
}
22. Give a worked example where a Template Method's createProduct() step is itself a Factory Method overridden by each ConcreteClass.
A ReportPipeline template calls createExporter() as one of its fixed steps, and each ConcreteClass, PdfReportPipeline or CsvReportPipeline, overrides that single Factory Method to return the exporter type it needs, while every other step of the pipeline stays identical.
abstract class ReportPipeline {
public final void run(Report report) {
Exporter exporter = createExporter(); // Factory Method hook
exporter.export(report);
}
protected abstract Exporter createExporter();
}
class PdfReportPipeline extends ReportPipeline {
@Override protected Exporter createExporter() { return new PdfExporter(); }
}
23. Compare Template Method to the Builder pattern: both involve a fixed sequence of steps, but they solve different problems. Explain the distinction.
Both patterns impose a fixed order of steps, but Template Method's steps run inside one method call on one already-instantiated object to perform an operation, while Builder's steps run across multiple calls, from client code, to incrementally assemble the fields of a single new object that does not exist yet until build() is called.
Template Method is about varying the behavior of an operation; Builder is about varying the construction of an object's internal state. It is entirely possible, and common, to use a Builder to construct the ConcreteClass instances that a Template Method later operates on, since the two patterns solve orthogonal problems.
24. Compare Template Method to Chain of Responsibility: both involve a sequence of operations, but explain why their intent and structure differ.
Template Method's sequence is a fixed, closed list of named steps known at compile time, defined once in the base class and never reconfigured at runtime. Chain of Responsibility's sequence is an open, dynamically assembled list of independent handler objects, each deciding whether to process a request or pass it along, and the chain's membership and order can be reconfigured at runtime without touching any handler's code.
The intents differ too: Template Method exists to reuse a fixed algorithm's structure across variants; Chain of Responsibility exists to decouple a request's sender from whichever handler eventually processes it, without the sender needing to know the chain's composition at all.
25. Compare Template Method to Command: could a sequence of Command objects executed in order be seen as a flexible alternative to a rigid Template Method? Discuss trade-offs.
Yes: a List<Command> executed in order is a legitimate, more flexible alternative when the "steps" need to be added, removed, reordered, logged, undone, or queued at runtime, none of which a fixed inheritance-based template method supports without recompiling. Each Command becomes an independent, individually testable, individually reusable object, whereas a Template Method's steps only exist as protected methods tied to one class hierarchy.
The trade-off is overhead and indirection: a short, fixed, two- or three-step sequence rarely benefits from being modeled as discrete Command objects, and the added ceremony of a command list, an invoker, and possibly an undo stack is only worth paying for when that dynamic flexibility is an actual requirement, not a hypothetical one.
26. What are the downsides of the Template Method pattern, specifically the fragile base class problem, and how does it manifest in a deep inheritance hierarchy?
The fragile base class problem occurs when a seemingly safe change to the AbstractClass, such as adjusting the order of two fixed steps or changing what a shared protected field holds at a given point, silently breaks a subclass several levels down the hierarchy that depended on the old behavior, even though the subclass's own code never changed.
In a deep Template Method hierarchy this gets worse the more subclasses and intermediate abstract subclasses exist, because the author of the top-level base class cannot realistically know or verify every assumption every downstream subclass has made about timing, field state, or call order.
27. Explain why unit testing a Template Method hierarchy can be harder than testing a Strategy-based design, and what specifically makes it harder.
Testing a single overridden step in isolation usually requires instantiating an entire ConcreteClass, or writing a throwaway test subclass, because the step is a protected method tied to the class hierarchy rather than a standalone object with its own interface. There is no way to hand the test a bare "strategy" object representing just that one step; the whole subclass, and by extension the whole fixed algorithm, comes along with it.
A Strategy-based design, by contrast, lets you construct and test the varying behavior's implementation directly, with no dependency on the surrounding fixed algorithm at all, since it is just an object implementing a small interface.
28. Describe two different approaches to testing a Template Method hierarchy: testing via a minimal concrete test subclass overriding only the hooks needed, versus testing each ConcreteClass's overridden steps directly.
The first approach creates a minimal, test-only subclass that overrides just enough abstract methods with simple, controllable stub logic to exercise the fixed algorithm itself, verifying the base class's orchestration, ordering, and exception handling independent of any real subclass's business logic.
class TestPipeline extends DataPipeline {
boolean saved = false;
@Override protected Object parseData(Object raw) { return raw; }
@Override protected void saveResult(Object parsed) { saved = true; }
}
@Test
void runsFullSequenceInOrder() {
TestPipeline pipeline = new TestPipeline();
pipeline.run();
assertTrue(pipeline.saved);
}
The second approach tests each real ConcreteClass's overridden methods directly by calling them as ordinary protected-turned-package-private or extracted methods, verifying that specific subclass's business logic in isolation from the base class's fixed orchestration.
29. Design a report-generation Template Method with fixed steps to fetch data and render a header/footer, and an overridable step to format the body as PDF, CSV, or HTML.
The fixed steps, fetching the underlying data and rendering a consistent header and footer, are identical regardless of output format, so they belong in the AbstractClass and stay private or final. The one genuinely varying step, formatting the body, is abstract because there is no sensible default shared across PDF, CSV, and HTML rendering.
abstract class ReportGenerator {
public final Report generate(ReportData data) {
String header = renderHeader(data);
String body = renderBody(data); // abstract, format-specific
String footer = renderFooter(data);
return new Report(header + body + footer);
}
protected abstract String renderBody(ReportData data);
private String renderHeader(ReportData data) { return "..."; }
private String renderFooter(ReportData data) { return "..."; }
}
30. Walk through what happens, a subtle bug, when a subclass accidentally overrides a step that was meant to stay fixed, because the base class author forgot to mark it final.
If renderHeader() above were accidentally left non-final and public instead of private, a new ConcreteClass author might reasonably assume it is an intended extension point and override it, perhaps to add a per-tenant branding header. Every other report type continues using the shared header logic, but this one subclass silently diverges, and nothing in the type system flags the inconsistency.
Weeks later, a shared header formatting fix applied to the base class quietly fails to reach that one subclass's reports, and the bug is invisible in code review because the override looks like completely legitimate, intentional customization.
private or final, never leave it merely protected and unmarked out of convenience.31. Explain a bug where a hook's default no-op implementation silently does nothing because a subclass forgot to override it, and how that silence caused a production incident.
Suppose validateData() is a hook defaulting to return true, meaning "no extra validation needed." A new ConcreteClass handling untrusted, externally-submitted CSV uploads is added without anyone realizing this hook exists or that it should be overridden for this particular source, so malformed rows silently flow all the way through to saveResult() exactly as if they had been validated.
The incident surfaces only once corrupted downstream data is discovered, and root-causing it is painful precisely because nothing threw, logged, or failed loudly; the hook did exactly what its default promised, quietly, for a case that needed the opposite.
32. Explain how calling template steps out of order becomes possible, and dangerous, if the template method itself is not declared final or is exposed with visibility that lets a subclass override the orchestration method.
If the AbstractClass's run() method is left overridable, any ConcreteClass can override it entirely and call the fixed private-turned-protected steps in whatever order it likes, or skip some altogether. This defeats the pattern's core promise, that the sequence is guaranteed, and turns every subclass into a potential source of a completely different execution order that callers relying on the base contract have no way to detect statically.
class RogueSubclass extends DataPipeline {
@Override
public void run() { // overriding a non-final template method
saveResult(parseData(readData())); // skipped validateData() entirely
}
}
33. Explain why the template method's visibility is typically public final while the primitive operations/hooks are typically protected abstract or protected, and the reasoning behind each visibility choice.
The template method is public because it is the operation client code is meant to call, and final because its sequence must never be overridden. The primitive operations and hooks are protected because they are implementation details meant only for subclasses to supply or customize, not for external client code to call directly, bypassing the fixed orchestration entirely.
public abstract class AbstractClass {
public final void templateMethod() { stepOne(); stepTwo(); }
protected abstract void stepOne(); // subclass-only, not client-callable
protected void stepTwo() { } // hook, subclass-only
}
34. Design a base class for onboarding workflows (fixed steps: validate input, create account, fixed steps: send confirmation email, hookable step: assign default permissions per account type).
Validating input, creating the account record, and sending the confirmation email are identical for every account type, so they stay fixed in the base class. Assigning default permissions genuinely differs, an admin account needs broader defaults than a guest account, but most account types are fine with a reasonable baseline, making it a hook rather than an abstract method.
abstract class OnboardingWorkflow {
public final Account onboard(SignupRequest request) {
validate(request);
Account account = createAccount(request);
assignDefaultPermissions(account); // hook, has a sensible default
sendConfirmationEmail(account);
return account;
}
protected void assignDefaultPermissions(Account account) { account.grant(Role.BASIC_USER); }
private void validate(SignupRequest request) { }
private Account createAccount(SignupRequest request) { return new Account(); }
private void sendConfirmationEmail(Account account) { }
}
35. Design a Template Method-based ETL (extract-transform-load) framework where extract and load are fixed but transform is overridden per data source.
Extraction reads from whatever source is configured, and loading writes to the same fixed destination warehouse schema regardless of source, so both stay fixed. Transformation genuinely differs source by source, a legacy mainframe feed needs different field mapping than a modern JSON API feed, so it is the one abstract step.
abstract class EtlJob {
public final void run() {
RawRecords raw = extract(); // fixed
List<Record> transformed = transform(raw); // abstract, source-specific
load(transformed); // fixed
}
protected abstract List<Record> transform(RawRecords raw);
private RawRecords extract() { return null; }
private void load(List<Record> records) { }
}
36. How would you refactor a large if/else chain that branches on a "type" field, where each branch runs almost the same steps in the same order but differs in a couple of places, into a Template Method hierarchy?
First, identify the steps that are truly identical across every branch, extract them once into a base class as fixed private methods. Then identify the one or two places each branch genuinely diverges and turn those into abstract methods or hooks. Finally, replace each branch of the if/else with a ConcreteClass, and replace the branching call site with a factory that returns the correct ConcreteClass instance based on the type field, so the type check happens exactly once, at construction time, rather than being repeated on every call.
// before: if (type.equals("PDF")) {...} else if (type.equals("CSV")) {...}
// after:
ReportGenerator generator = ReportGeneratorFactory.forType(type);
generator.generate(data);
37. Explain how the Template Method pattern can grow a "fat" abstract base class over time as more and more hook methods are added, and how to recognize when this has gone too far.
Each new requirement tempts a developer to add "just one more hook" to the shared base class rather than reconsidering the design, and over years this accretes into a base class with a dozen or more overridable methods, most of which any given subclass ignores. New subclass authors then face the burden of reading through all of them to figure out which few actually matter for their case.
Warning signs it has gone too far: subclasses routinely override the same three or four hooks while leaving eight others untouched, the base class's Javadoc is longer than most subclasses' entire bodies, and adding a genuinely new variation requires touching several existing subclasses just to keep their overrides internally consistent.
38. What is the maximum practical number of variation points (hooks/abstract methods) a Template Method's abstract class should expose before you should reconsider the design? Explain your reasoning.
There is no hard universal number, but a common practical guideline is that once a base class exposes more than roughly four or five overridable steps, it is worth asking whether some of them should instead be grouped into a single injected Strategy or configuration object, because at that point most individual subclasses will only care about a small subset, and the remaining defaults just add noise.
The reasoning is about cognitive load: a reader trying to understand one ConcreteClass has to mentally track every hook it does not override just as much as the ones it does, since silence itself is meaningful, and that burden grows roughly linearly with the number of variation points in the base class.
39. Design a batch-job processing framework's abstract class with fixed steps for acquiring a lock, logging start/end, and an overridable step for the actual job logic.
Acquiring a distributed lock so only one instance runs the job, and logging start and end timestamps for observability, are cross-cutting concerns every job needs identically, so they are fixed and wrapped in a try/finally to guarantee the lock releases and the end log fires even if the job logic throws. The actual work performed is the one abstract method.
abstract class BatchJob {
public final void execute() {
log("START " + getClass().getSimpleName());
Lock lock = acquireLock();
try {
runJob(); // abstract
} finally {
lock.release();
log("END " + getClass().getSimpleName());
}
}
protected abstract void runJob();
private Lock acquireLock() { return null; }
private void log(String msg) { }
}
40. Explain how a Template Method can incorporate a "hook" that controls flow, such as a boolean-returning hook that decides whether an optional step should run at all.
A boolean-returning hook lets the fixed algorithm branch on subclass-supplied logic without exposing the branching decision itself as overridable, preserving the guarantee that the overall sequence and its conditions stay controlled entirely by the base class. The subclass only answers a yes/no question; the base class decides what to do with the answer.
public final void process(Order order) {
reserveInventory(order);
if (requiresManualReview(order)) { // boolean flow-control hook
flagForReview(order);
} else {
autoApprove(order);
}
}
protected boolean requiresManualReview(Order order) { return false; } // default: no review
41. A boolean hook named shouldValidate() defaults to false. Explain the specific risk this creates and why a "default to skip" hook is more dangerous than a "default to run" hook.
A hook that defaults to false for "should the safety-relevant step run" means every new subclass starts out unvalidated unless its author actively remembers to override it, and remembering is exactly the kind of thing new team members do not reliably do without being told. Contrast this with a hook defaulting to true, where the dangerous state, skipping validation, requires a deliberate, visible override that a reviewer is far more likely to question.
The general rule is to make the safe behavior the default and the risky behavior the one requiring an explicit opt-in, so that forgetting to override the hook fails toward safety rather than away from it.
protected boolean shouldValidate() { return true; } // safe default: validation runs unless a subclass opts out
// vs. the riskier shape:
protected boolean shouldValidate() { return false; } // dangerous default: silently skips validation
42. Compare overriding a protected step method in a subclass versus supplying a lambda for the same varying behavior. What changes structurally, and what stays the same?
What stays the same is the intent: one piece of behavior is being supplied to fill in a gap in an otherwise fixed algorithm. What changes is the mechanism: an override binds the behavior to a class at compile time through inheritance, requiring a new named type for every variant, while a lambda binds the behavior to a functional interface value passed at construction or call time, letting the same class supply different behavior on different instances.
| Aspect | Method override | Lambda / functional field |
|---|---|---|
| Binding time | Compile time, fixed per subclass | Runtime, fixed per instance |
| New variant costs | A whole new subclass | One new lambda expression |
| Access to shared state | Direct, via inherited protected fields | Only what is captured or passed in explicitly |
| Testability | Requires instantiating the subclass | Test the lambda's logic standalone |
43. Rewrite a Template Method class so its one varying step is supplied as a functional interface field passed to the constructor, instead of being an abstract method subclasses override. Show before and after.
The fixed steps stay exactly where they were; only the varying step moves from an abstract method that requires subclassing into a functional field that requires no subclass at all, just an instance constructed with the right lambda.
// Before: Template Method via subclassing
abstract class DataPipeline {
public final void run(Object raw) { save(parse(raw)); }
protected abstract Object parse(Object raw);
private void save(Object parsed) { }
}
class JsonPipeline extends DataPipeline {
@Override protected Object parse(Object raw) { return JsonParser.parse((String) raw); }
}
// After: same shape, no subclass required
class ConfigurablePipeline {
private final Function<Object, Object> parser;
ConfigurablePipeline(Function<Object, Object> parser) { this.parser = parser; }
public final void run(Object raw) { save(parser.apply(raw)); }
private void save(Object parsed) { }
}
ConfigurablePipeline jsonPipeline = new ConfigurablePipeline(raw -> JsonParser.parse((String) raw));
This is the exact same trade discussed for Template Method versus Strategy, made concrete: one instance can now be reconfigured with a new parser at runtime, and testing the parsing logic no longer requires a throwaway subclass.
44. Explain how Java 8's default methods on interfaces make it possible to implement a Template Method without an abstract class at all, and what limitations this approach has.
An interface can declare the fixed sequence as a default method that calls one or more other abstract interface methods, letting an implementing class supply only the varying pieces while inheriting the orchestration for free, exactly like an abstract base class's template method, except delivered through an interface.
interface Exporter {
default void export(Report report) { // the "template", but on an interface
String body = renderBody(report);
write(body);
}
String renderBody(Report report); // must be implemented
private void write(String body) { /* shared default logic */ }
}
The limitation is that interfaces cannot declare instance fields, so any step that needs to accumulate or share mutable state across the sequence has nowhere to live without pushing that state into method parameters or a separate object, and the default method itself cannot be marked final to lock the sequence the way a class method can, since Java does not support final default methods.
45. Design a fully interface-based Template Method, using only default and abstract interface methods, and explain when this is preferable to an abstract class version.
An interface-based template is preferable when the implementing classes already extend an unrelated class for some other reason, since Java allows implementing multiple interfaces but extending only one class, and when the "steps" genuinely need no shared mutable state between them, only the input parameters already available at each call.
interface Greeter {
default void greet(String name) {
System.out.println(salutation() + ", " + name + "!");
}
String salutation(); // abstract
}
class FormalGreeter implements Greeter {
@Override public String salutation() { return "Good day"; }
}
Where an abstract class version still wins is when the fixed steps need private helper methods, protected shared fields, or a non-public constructor to enforce controlled instantiation, none of which interfaces support, so the choice ultimately comes down to whether the design needs state and encapsulation or can get by on pure stateless dispatch.
46. Is implementing Comparable's compareTo() method an example of the Template Method pattern? Explain why a single abstract method interface is closer to Strategy than to Template Method.
No: Comparable declares exactly one method, compareTo(), with no fixed surrounding sequence of other steps calling into it from a shared base class. Template Method requires a multi-step skeleton where some steps are fixed and at least one is variable; a single-method interface has no skeleton to fix, it is simply an interchangeable piece of behavior, which is precisely the shape of Strategy, not Template Method.
Where Template Method does show up nearby is in code that calls compareTo(), such as Collections.sort(), whose fixed sorting algorithm is the skeleton and your compareTo() implementation is the injected comparison step, but that fixed algorithm lives in Collections/Arrays, composed with your object via the interface, not inherited through a base class, which again is Strategy's composition mechanism rather than Template Method's inheritance mechanism.
47. Explain how java.util.concurrent.AbstractExecutorService uses Template Method to implement submit() and invokeAll() in terms of the abstract execute() method.
AbstractExecutorService implements the higher-level submit(), invokeAll(), and invokeAny() methods entirely in terms of the one abstract method every ExecutorService ultimately needs, execute(Runnable), inherited from Executor. It wraps each submitted task in a RunnableFuture via newTaskFor(), then hands that wrapped task to execute(), so a concrete executor only has to decide how a bare Runnable actually gets run, thread pool dispatch, a direct call, or anything else, and inherits fully correct Future-returning submission semantics for free.
public abstract class AbstractExecutorService implements ExecutorService {
public Future<?> submit(Runnable task) {
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask); // delegates to the one abstract/overridden primitive
return ftask;
}
// execute(Runnable) is declared on Executor, supplied by the concrete subclass
}
48. Design a realistic enterprise Template Method that combines one mandatory abstract step and one optional hook step in the same fixed sequence, and explain why each was categorized the way it was.
Consider a PaymentProcessor whose fixed sequence authorizes funds, then must delegate the mandatory step of actually charging the specific payment rail, which has no sensible shared default across credit card, ACH, and wallet providers, so it is abstract. Afterward it runs an optional receipt-customization hook, since most providers are fine with a standard receipt but a couple need a provider-specific addendum.
abstract class PaymentProcessor {
public final Receipt process(Payment payment) {
authorize(payment); // fixed
ChargeResult result = charge(payment); // abstract: mandatory, no shared default
Receipt receipt = buildReceipt(result); // fixed
customizeReceipt(receipt, result); // hook: optional, has a no-op default
return receipt;
}
protected abstract ChargeResult charge(Payment payment);
protected void customizeReceipt(Receipt receipt, ChargeResult result) { /* no-op default */ }
private void authorize(Payment payment) { }
private Receipt buildReceipt(ChargeResult result) { return new Receipt(result); }
}
49. Design an authentication workflow using Template Method with fixed steps for rate-limiting and audit logging, and an abstract step for verifying credentials against a specific provider (LDAP, database, OAuth).
Rate-limiting login attempts and writing an audit log entry are security and compliance concerns that must behave identically no matter which credential provider is in use, so they are fixed steps wrapping the variable part. Verifying credentials is abstract because an LDAP bind, a database password hash comparison, and an OAuth token exchange share no common implementation whatsoever.
abstract class AuthWorkflow {
public final AuthResult authenticate(Credentials creds) {
checkRateLimit(creds.principal());
AuthResult result = verifyCredentials(creds); // abstract, provider-specific
auditLog(creds.principal(), result);
return result;
}
protected abstract AuthResult verifyCredentials(Credentials creds);
private void checkRateLimit(String principal) { }
private void auditLog(String principal, AuthResult result) { }
}
class LdapAuthWorkflow extends AuthWorkflow {
@Override protected AuthResult verifyCredentials(Credentials creds) { return LdapClient.bind(creds); }
}
50. The authentication workflow above needs to support switching credential providers at runtime per tenant. Explain why this forces a migration from Template Method to Strategy, and show the refactor.
Template Method's variation point is bound to a subclass chosen at object-construction time, one fixed provider per instance's class. Once the requirement becomes "the same running service must pick a provider per tenant, per request, without redeploying," the variation needs to be swappable at runtime on a single shared instance, which is exactly what Strategy's composition, injecting a provider object, supports and inheritance does not.
interface CredentialVerifier { AuthResult verify(Credentials creds); }
class AuthWorkflow { // no longer abstract; one class, many injected strategies
public final AuthResult authenticate(Credentials creds, CredentialVerifier verifier) {
checkRateLimit(creds.principal());
AuthResult result = verifier.verify(creds); // composition, chosen per call
auditLog(creds.principal(), result);
return result;
}
private void checkRateLimit(String principal) { }
private void auditLog(String principal, AuthResult result) { }
}
51. Design a turn-based game AI using Template Method, with fixed steps for checking game-over conditions and updating shared state, and an abstract step for deciding the next move.
Checking whether the game has ended and applying a chosen move to the shared board state must happen identically for every AI difficulty or personality, so they stay fixed. Deciding what move to make is the one place a "random" AI, a "greedy" AI, and a "minimax" AI genuinely diverge, so it is the sole abstract method.
abstract class GameAiTurn {
public final void takeTurn(Board board) {
if (isGameOver(board)) return; // fixed
Move move = decideMove(board); // abstract, personality-specific
applyMove(board, move); // fixed
}
protected abstract Move decideMove(Board board);
private boolean isGameOver(Board board) { return board.isTerminal(); }
private void applyMove(Board board, Move move) { board.apply(move); }
}
52. Explain the risk when an exception thrown by one step in the middle of a Template Method's sequence leaves shared state half-updated, and how to reason about which invariants must survive a failure.
If a fixed step updates a shared field, an abstract step throws, and a later fixed step assumed the earlier update would be immediately followed by a corresponding cleanup or commit step that never gets to run, the object is left in a state no single step's contract anticipated, half of one operation and none of the next. This is dangerous specifically because no individual step's code is wrong in isolation; the bug lives entirely in the interaction between steps under a failure path nobody tested.
The right way to reason about it is to identify, for each step, what invariant must hold if execution stops right after that step throws, and to make sure any step that establishes a temporary, must-be-followed-up state does so inside a try block whose finally restores or completes it regardless of what the later step does.
53. Show how to guarantee a cleanup step always executes in a Template Method, even if an earlier abstract step throws, using try/finally, and explain why relying on the caller to clean up is not sufficient.
Wrapping the variable step in try and putting the cleanup in finally inside the template method itself guarantees the cleanup runs regardless of which subclass is in play or whether its step throws a checked or unchecked exception, because the guarantee lives in the one place every subclass shares, not in code each subclass would otherwise have to remember to duplicate.
public final void process(Resource resource) {
acquire(resource);
try {
handle(resource); // abstract, may throw
} finally {
release(resource); // always runs, subclass cannot forget or skip it
}
}
protected abstract void handle(Resource resource);
Relying on the caller to clean up instead pushes a correctness requirement outside the class that owns the resource, and any caller that forgets, or any code path that exits early, leaks the resource with no compiler or runtime signal that anything went wrong.
54. Discuss the design trade-off of declaring a hook method to throw a checked exception, and how this interacts with functional-interface-based alternatives that cannot easily throw checked exceptions.
Declaring protected abstract void step() throws IOException forces every overriding subclass to either handle or further declare that exception, which is honest when the step genuinely involves I/O, but it also means the enclosing template method must itself declare or handle it, propagating the checked-exception requirement up through every caller of the fixed algorithm even when most concrete steps never actually throw.
This gets noticeably worse if the same step is later reshaped into a functional interface parameter, since standard functional interfaces like Function and Supplier do not declare checked exceptions in their abstract method signatures, forcing either a custom functional interface that does, or wrapping the checked exception in an unchecked one at the lambda boundary, which is why many modern APIs standardize on unchecked exceptions specifically to stay compatible with lambdas.
55. Compare passing intermediate results between template steps via shared protected instance fields versus passing them explicitly as method parameters and return values.
Shared fields let each step read and write a common piece of state without every method signature growing to carry it explicitly, which keeps signatures short but makes the data flow implicit: to know what a given step actually depends on, a reader has to trace which fields it touches rather than simply reading its parameter list. Passing state as parameters and return values keeps the data flow explicit and each step's contract self-documenting, at the cost of longer signatures and needing to thread a value through steps that do not otherwise use it.
// Shared field: implicit dependency
abstract class A { private Object parsed; public final void run(Object raw) { parsed = parse(raw); save(); } }
// Explicit parameter: dependency visible in the signature
abstract class B { public final void run(Object raw) { save(parse(raw)); } }
As a rule, explicit parameters scale better once a hierarchy grows past a couple of steps and a couple of subclasses, because the implicit-field approach's hidden coupling is exactly what makes fragile base class bugs hard to diagnose later.
56. Walk through a bug caused by a shared mutable field used to pass state between template steps, where a subclass's overridden step reads the field before an earlier fixed step has finished setting it up.
Suppose the base class sets this.context = buildContext(request) partway through the fixed sequence, and a subclass overrides an earlier step that, for its own unrelated reason, also happens to read this.context, perhaps to log something. If a later refactor reorders the fixed steps so the context is built even one line later than before, that subclass's earlier step now reads a stale or null context field, with no compiler error, because the field was always technically accessible, just implicitly assumed to be populated by then.
57. Under what conditions does a Template Method's AbstractClass need to be thread-safe, and how do shared instance fields used to pass step-to-step state threaten that requirement?
Thread safety becomes a requirement the moment a single instance of a ConcreteClass is shared across multiple threads, which is common for framework-managed singletons such as a Spring bean registered with the default singleton scope. If the template method stores intermediate results in an instance field rather than a local variable, two threads calling the template method concurrently will interleave writes and reads to that same field, corrupting each other's in-flight computation even though each thread individually calls only public, seemingly independent methods.
The safest default is to keep every piece of per-invocation state as a local variable or method parameter rather than an instance field, reserving instance fields strictly for state that is genuinely shared and properly synchronized, such as an injected, thread-safe collaborator configured once at construction.
58. Design a file-exporter Template Method where opening and closing the output writer are fixed steps and writing the actual content is the one abstract step.
Opening the writer against the configured output path and closing it afterward, guaranteeing closure even if writing fails, are identical for every export format, so they are fixed and wrapped in try-with-resources. Writing the content is abstract because a CSV exporter, an XML exporter, and a fixed-width exporter format the same underlying data completely differently.
abstract class FileExporter {
public final void exportTo(Path path, Data data) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
writeContent(writer, data); // abstract
}
}
protected abstract void writeContent(BufferedWriter writer, Data data) throws IOException;
}
59. Describe how you would audit an existing Template Method base class to find hooks that no subclass actually overrides, and explain why removing dead extension points is worth doing.
An IDE's "find usages" or "find overriding methods" feature run on every hook in the base class quickly separates hooks that every subclass ignores, meaning the default is universally correct, from hooks that are actively overridden and therefore genuinely load-bearing. Static analysis and a project-wide search for @Override annotations on the hook's exact signature across all known subclasses gives the same answer more mechanically, and is worth automating if the hierarchy is large or spans multiple modules.
Removing a hook nobody overrides simplifies the base class's public contract, shrinks the surface new subclass authors have to read and reason about, and removes one more place a future change could accidentally introduce behavior nobody asked for; the risk is only in hooks used by code outside the audited codebase, such as a published library, where "unused" cannot be verified exhaustively.
60. You are reviewing a pull request that adds a new step to a Template Method's fixed sequence, used by fifteen existing subclasses. What specifically would you check before approving it?
First, whether the new step is inserted as a fixed private call, a mandatory abstract method, or a hook with a safe default, since only the hook option is guaranteed not to break any of the fifteen existing subclasses at compile time. Second, where exactly it is inserted in the sequence, and whether any existing subclass's overridden steps implicitly assumed the old ordering or the old absence of this step. Third, whether the new step can throw, and whether that exception type is compatible with every existing caller of the template method.
61. Explain precisely why adding a new abstract method to an AbstractClass that already has fifteen ConcreteClass subclasses is a breaking change, while adding a hook with a default is not.
A new abstract method has no implementation, so every existing ConcreteClass that does not already override it becomes an incomplete implementation of an abstract class, which the compiler rejects outright: all fifteen fail to compile until each one is manually given a new override, even the ones that would have been perfectly happy with any reasonable default.
abstract class A { protected abstract void newStep(); } // breaks every existing subclass, no default exists
abstract class A2 { protected void newStep() { /* safe default */ } } // compiles unchanged for every existing subclass
A hook with a concrete default body compiles cleanly against every existing subclass unchanged, because Java's inheritance rules only require overriding abstract members, never concrete ones, which is exactly why "always add new extension points as hooks, never as new abstract methods" is close to an ironclad rule for evolving a published base class.
62. Compare the compatibility cost of adding a new variation point to an existing Template Method hierarchy versus adding one to an existing Strategy-based design.
In Template Method, adding a new mandatory variation point means adding a new abstract method, which as established breaks every existing subclass at compile time unless it is instead added as a hook. In Strategy, adding a new variation point usually means adding a new method to the strategy interface, which has exactly the same problem for every existing implementation of that interface, unless the new method is added as a default method on the interface with a sensible body, the interface-level equivalent of a hook.
| Change | Template Method cost | Strategy cost |
|---|---|---|
| New mandatory step, no default | Every subclass fails to compile | Every implementation fails to compile |
| New step with a safe default | Add as a hook; existing subclasses unaffected | Add as an interface default method; existing implementations unaffected |
| Swapping the variation at runtime | Not possible without a new subclass instance | Just inject a different strategy object |
The two patterns turn out to have nearly identical compatibility mechanics once Java 8's default methods are in the picture; the practical difference between them is really about runtime flexibility and state sharing, not about how safely they can be extended.
63. Give a concrete before-and-after example showing how choosing a hook instead of a new abstract method avoids breaking an established Template Method hierarchy.
Suppose ReportGenerator already has ten subclasses in production and a new requirement appears: some reports need a watermark applied. Adding protected abstract void applyWatermark(Report report) would immediately break all ten existing subclasses, forcing every team to touch code that has nothing to do with watermarks just to keep the build green.
abstract class ReportGenerator {
public final Report generate(ReportData data) {
Report report = build(data);
applyWatermark(report); // added as a hook, not an abstract method
return report;
}
protected void applyWatermark(Report report) { /* default: no watermark, matches prior behavior exactly */ }
private Report build(ReportData data) { return new Report(data); }
}
Adding it as a hook whose default reproduces the prior behavior exactly, no watermark, means all ten existing subclasses compile and run completely unchanged, and only the one or two new subclasses that actually need a watermark have to override anything at all.
64. Describe an incremental strategy for migrating a large Template Method hierarchy toward Strategy-based composition without a risky big-bang rewrite.
Start by introducing a Strategy interface that mirrors one specific varying step, and change the AbstractClass to delegate that one step to an injected strategy object instead of an abstract method, while every other step stays exactly as it was. Existing subclasses can keep working unmodified for a transition period by having the base class default the strategy field to an adapter that simply calls the old abstract method, so nothing breaks on day one.
Once that first step has proven the pattern works and the team is comfortable with it, repeat for the next varying step, one at a time, and only after every step has been extracted does the inheritance hierarchy collapse down to a single concrete class configured entirely by injected strategies, at which point the old subclasses can be deleted and replaced by strategy instances.
65. Describe a realistic scenario where Template Method was the wrong choice in hindsight, and explain what signal, visible only after the fact, revealed the mistake.
A notification system started with three channels, email, SMS, and push, sharing a fixed template for building and sending a message, which felt clean at the time. Eighteen months later the product needed to send the same notification through two channels simultaneously for some users, and a single template-method instance bound to one subclass per channel had no way to express "run these two channel behaviors together for this one call," since the variation was locked to which class was instantiated, not to which behaviors a given send operation should combine.
The signal that revealed the mistake only after the fact was a requirement for runtime composability that simply did not exist at design time; nothing about the original three-channel design was wrong given what was known then, which is the honest, non-hindsight-biased lesson: Template Method is often the right call for the requirements in front of you and still needs revisiting when requirements genuinely change shape.
66. Explain how a subclass's overridden hook or abstract method can violate the Liskov Substitution Principle, and give a concrete example involving a strengthened precondition.
LSP requires that a subclass be substitutable anywhere the base class is expected, which for a Template Method step means the override must accept at least everything the base class's contract promised to pass it, and must not impose new preconditions the fixed algorithm never guaranteed to satisfy. A subclass that overrides processItem(Item item) and immediately throws if item.getCategory() == null, when the base class's contract never promised categories would always be populated, has strengthened the precondition and will fail for perfectly legitimate inputs the base algorithm was designed to handle.
protected void processItem(Item item) {
if (item.getCategory() == null) throw new IllegalStateException(); // strengthened precondition, violates LSP
// ...
}
67. How would you document a Template Method's contract using Javadoc so that subclass authors understand exactly what each step is allowed to assume and required to guarantee?
Javadoc's @implSpec tag, introduced for exactly this purpose, documents the implementation-level contract a method must satisfy for subclasses, as distinct from @apiNote's usage guidance and the general description's client-facing behavior. For a Template Method step, the @implSpec should state precisely what state is guaranteed to be set up before this step runs, what the step is required to return or guarantee on exit, and whether it may throw.
/**
* Parses the raw payload into a domain object.
*
* @implSpec Called after {@code readData()} has populated the raw payload;
* implementations must return a non-null result or throw {@link ParseException}.
* Must not have side effects on shared fields other than the returned value.
*/
protected abstract Object parseData(Object raw) throws ParseException;
68. Show how to add a runtime assertion in the fixed part of a Template Method to validate an invariant that an overridden step must satisfy, catching a contract violation early rather than letting it corrupt later steps.
Rather than trusting silently that every subclass's override honors an unenforceable Javadoc contract, the fixed algorithm can check the result of a step immediately after calling it and fail fast with a clear message, turning a subtle downstream corruption into an obvious, immediate, easy-to-diagnose failure at the exact point the contract was actually broken.
public final void run(Object raw) {
Object parsed = parseData(raw);
if (parsed == null) {
throw new IllegalStateException(getClass() + ".parseData() must not return null, per its @implSpec");
}
save(parsed);
}
69. Walk through a bug where an overridden step is accidentally invoked twice within a single run of the template method, and how this typically happens.
This most often happens after a refactor: a developer extracts part of the fixed sequence into a new private helper method, and that helper both calls the abstract step directly and is itself called from a spot where the step was already being invoked separately, so on the next release the step silently fires twice per run without any single line of the diff looking wrong in isolation.
public final void run(Order order) {
applyDiscount(order); // call site 1
Order enriched = enrich(order);
// enrich() internally also calls applyDiscount(order) again by mistake
}
private Order enrich(Order order) { applyDiscount(order); return order; } // duplicate call, easy to miss in review
The fix is defensive as much as corrective: a step with observable side effects, like applying a discount, should be idempotent where practical, or the fixed sequence should be restructured so each step is called from exactly one place, verified by a unit test asserting the call count.
70. Explain specifically why the individual steps of a Template Method are declared protected rather than public, beyond the general "hide implementation details" reasoning.
Beyond ordinary encapsulation, protected here specifically prevents external client code from calling an individual step directly and out of the sequence the template method guarantees, which would let a caller execute saveResult() without ever having called parseData() first, silently violating every assumption the algorithm's designer made about call order and state. Making the steps public would advertise them as independently callable operations, which is exactly the opposite of what the pattern promises.
public final void run() { parseData(); saveResult(); } // the only sanctioned entry point
protected void saveResult() { } // protected: cannot be called out-of-sequence by external client code
71. What is the risk when a ConcreteClass widens a protected step's visibility to public in its own override, and can Java even allow this?
Java allows widening visibility on override, protected to public is legal, but doing so reintroduces exactly the risk protected was meant to prevent for that one subclass: external code holding a reference typed as the ConcreteClass, rather than the AbstractClass, can now call that one step directly, out of the guaranteed sequence, even though every sibling subclass still enforces the original protected restriction.
abstract class Base { protected abstract void step(); }
class Widened extends Base {
@Override public void step() { } // legal: widening protected to public, but now callable out of sequence
}
72. Explain how to test just the fixed skeleton's orchestration logic, independent of any real ConcreteClass's business logic, using a minimal stub subclass.
The idea is to create a subclass whose only job is recording what happened and when, with trivial stub bodies for every abstract method, so the test can assert purely on the base class's behavior: did it call the steps in the right order, did it call the cleanup step even after a simulated failure, did it correctly propagate or suppress an exception.
class RecordingPipeline extends DataPipeline {
final List<String> calls = new ArrayList<>();
@Override protected Object parseData(Object raw) { calls.add("parse"); return raw; }
@Override protected void saveResult(Object parsed) { calls.add("save"); }
}
@Test
void skeletonRunsStepsInOrder() {
RecordingPipeline pipeline = new RecordingPipeline();
pipeline.run(null);
assertEquals(List.of("parse", "save"), pipeline.calls);
}
73. Write a JUnit test that verifies a Template Method calls its steps in the exact expected order, using a shared list to record call order.
Recording each step's name into a shared, ordered list as it runs, then asserting the full list matches the expected sequence exactly, verifies both that every expected step ran and that none ran out of order, which a simple boolean "was it called" assertion per step would miss entirely.
class OrderTrackingPipeline extends DataPipeline {
final List<String> order = new ArrayList<>();
@Override protected Object parseData(Object raw) { order.add("parseData"); return raw; }
@Override protected boolean validateData(Object parsed) { order.add("validateData"); return true; }
}
@Test
void callsStepsInFixedOrder() {
OrderTrackingPipeline pipeline = new OrderTrackingPipeline();
pipeline.run(new Object());
assertEquals(List.of("parseData", "validateData"), pipeline.order);
}
74. Show how to use a Mockito spy and InOrder verification to test that a real ConcreteClass's steps are invoked in the correct order by the inherited template method.
Wrapping a real ConcreteClass instance in a Mockito spy lets the test verify actual method invocations on the real object without replacing its logic, and Mockito's InOrder verifier specifically checks that a sequence of calls happened in the stated order, which is exactly the guarantee a Template Method exists to provide.
@Test
void invokesStepsInOrderOnRealSubclass() {
CsvDataPipeline pipeline = spy(new CsvDataPipeline());
pipeline.run("a,b,c");
InOrder inOrder = inOrder(pipeline);
inOrder.verify(pipeline).parseData(any());
inOrder.verify(pipeline).validateData(any());
}
This approach is valuable specifically when you want to verify the orchestration contract against a real, production subclass rather than a hand-written stub, catching cases where a real override's side effects might otherwise mask an ordering bug.
75. Explain how Spring Batch's chunk-oriented Step processing (read-process-write, with a fixed transaction boundary) reflects Template Method thinking, and design a simplified version.
Spring Batch's chunk step fixes the loop structure, read one item, process it, accumulate a chunk, write the chunk, commit the transaction, and repeat, while the actual reading, processing, and writing logic are supplied as separate, pluggable ItemReader, ItemProcessor, and ItemWriter objects. The fixed part is the chunking and transaction boundary; the variable part is what happens to each item.
class SimpleChunkStep<T> {
public final void execute(Supplier<T> reader, Function<T, T> processor, Consumer<List<T>> writer, int chunkSize) {
List<T> chunk = new ArrayList<>();
T item;
while ((item = reader.get()) != null) {
chunk.add(processor.apply(item));
if (chunk.size() == chunkSize) { writer.accept(chunk); chunk.clear(); } // fixed transaction boundary
}
if (!chunk.isEmpty()) writer.accept(chunk);
}
}
Notably this is, again, Template-Method-shaped intent delivered through Strategy-style injected collaborators rather than subclassing, the same pattern seen in Spring's *Template classes.
76. Design a fixed REST client lifecycle template: building the request and handling common HTTP-level errors are fixed, while parsing the response body into a domain object is the one abstract step.
Building the outgoing request with shared headers, authentication, and timeouts, then checking the response's HTTP status for common error conditions like 401 or 500 and translating them into a consistent exception type, is identical across every endpoint a client calls, so those steps are fixed. Parsing the specific JSON or XML response body into a domain object is abstract because every endpoint returns a different shape.
abstract class RestClientCall<T> {
public final T execute(HttpClient client, String url) throws IOException {
HttpRequest request = buildRequest(url); // fixed
HttpResponse<String> response = send(client, request); // fixed
checkForErrors(response); // fixed
return parseResponseBody(response.body()); // abstract
}
protected abstract T parseResponseBody(String body);
private HttpRequest buildRequest(String url) { return HttpRequest.newBuilder(URI.create(url)).build(); }
private HttpResponse<String> send(HttpClient client, HttpRequest request) throws IOException { return null; }
private void checkForErrors(HttpResponse<String> response) { }
}
77. Compare hand-rolling a REST client Template Method like the one above with using Spring's RestTemplate or WebClient directly. When does hand-rolling still make sense?
Spring's RestTemplate and WebClient already provide the fixed request-building, error-translation, and response-conversion skeleton, letting callers supply only a response type or a converter, so reaching for either first is almost always less code and better tested than a hand-rolled equivalent.
| Approach | When it fits | Cost |
|---|---|---|
| RestTemplate / WebClient | Standard HTTP semantics, JSON/XML bodies, typical error handling | Framework dependency; less control over unusual protocol quirks |
| Hand-rolled Template Method client | A proprietary wire protocol, unusual auth handshake, or a non-Spring codebase | You own writing and maintaining every fixed step yourself |
Hand-rolling still makes sense specifically when the "common" error-handling and request-building logic is not actually the common Spring case, such as integrating with a legacy SOAP-like service with its own bespoke envelope and fault format that a general-purpose template does not model well.
78. Explain how Maven's plugin execution lifecycle, via AbstractMojo's execute() method, reflects the Template Method pattern.
Maven's build lifecycle itself is a fixed, ordered sequence of phases, validate, compile, test, package, and so on, and each phase invokes whichever plugin goals are bound to it by calling that goal's execute() method, declared abstract on AbstractMojo. Maven's build engine is the "Hollywood" side, deciding when each goal's execute() runs relative to every other bound goal; the plugin author only supplies what happens inside that one method.
public class MyCustomMojo extends AbstractMojo {
@Override
public void execute() throws MojoExecutionException {
getLog().info("Running custom build step");
// plugin-specific logic only; Maven owns when this runs in the lifecycle
}
}
79. Design a CI/CD pipeline stage abstraction using Template Method: checkout and notification are fixed, while build, test, and deploy logic vary per project type.
Checking out source code and sending a success or failure notification are identical regardless of what kind of project is being built, so they are fixed and wrap the variable middle section in a try/catch to guarantee the notification fires either way. The build, test, and deploy steps genuinely differ between a Java Maven project, a Node.js project, and a Docker-based project.
abstract class PipelineStage {
public final void run(Project project) {
checkout(project); // fixed
try {
build(project); // abstract
test(project); // abstract
deploy(project); // abstract
notify(project, true);
} catch (Exception e) {
notify(project, false); // fixed, guaranteed
throw new PipelineException(e);
}
}
protected abstract void build(Project project);
protected abstract void test(Project project);
protected abstract void deploy(Project project);
private void checkout(Project project) { }
private void notify(Project project, boolean success) { }
}
80. Explain why an AbstractClass in a Template Method hierarchy is often given a protected, rather than public, constructor, and what this idiom communicates to future developers.
A protected constructor on the abstract base class communicates directly that the class exists purely to be extended, never instantiated on its own, reinforcing at the language level what the class already being abstract enforces at the compiler level for direct instantiation, but additionally documenting intent for any subclass constructor that might otherwise assume a public constructor implies standalone usability.
public abstract class AbstractClass {
protected AbstractClass(Config config) { this.config = config; } // signals: extend me, never instantiate directly
private final Config config;
}
It also gives the base class author a place to enforce invariants that every subclass must satisfy at construction time, such as validating a required configuration object, guaranteeing every ConcreteClass instance starts from a consistent, valid base state before any step ever runs.
81. Design a comprehensive order-processing Template Method that combines validation, inventory reservation, a mandatory payment-charging step, and an optional loyalty-points hook, in one coherent example.
Validating the order and reserving inventory are universal, fixed prerequisites for any order regardless of payment method or customer tier. Charging payment is abstract because credit card, wallet, and invoice-based billing have nothing in common to default to. Awarding loyalty points is a hook because most order types have no loyalty program attached, but a subclass for enrolled customers can override it.
abstract class OrderProcessor {
public final Receipt process(Order order) {
validate(order); // fixed
reserveInventory(order); // fixed
ChargeResult charge = chargePayment(order); // abstract
awardLoyaltyPoints(order, charge); // hook, default no-op
return buildReceipt(order, charge); // fixed
}
protected abstract ChargeResult chargePayment(Order order);
protected void awardLoyaltyPoints(Order order, ChargeResult charge) { /* default: no program */ }
private void validate(Order order) { }
private void reserveInventory(Order order) { }
private Receipt buildReceipt(Order order, ChargeResult charge) { return new Receipt(order, charge); }
}
82. Walk through a bug where a fixed step has an undocumented side effect that a hook silently depends on, creating a hidden coupling that breaks the moment someone reorders the steps.
Suppose reserveInventory() quietly sets a reservationId field as a side effect nobody documented, and a hook overridden by one subclass, awardLoyaltyPoints(), happens to read that field to log which reservation earned the points. Nothing in either method's signature reveals this dependency; it works purely by accident of the current step ordering.
Months later, a developer reorders the fixed sequence to reserve inventory after charging payment instead of before, a change that looks completely safe reading either method in isolation, and the loyalty-points hook silently starts reading a stale or unset reservationId, with no compiler warning and no failing test, because the coupling was never expressed anywhere a test or a type system could catch it.
83. Refactor the order-processing example so that intermediate results flow through an explicit context object passed to each step, instead of shared instance fields, and explain the benefit.
Introducing a single context object that each step receives and can read from or write into makes every dependency between steps visible in one place, the context's fields, rather than scattered implicitly across the class's own instance fields, and it also makes the AbstractClass itself stateless and safely reusable across concurrent calls.
abstract class OrderProcessor {
public final Receipt process(Order order) {
OrderContext ctx = new OrderContext(order);
validate(ctx);
reserveInventory(ctx);
chargePayment(ctx); // abstract, reads/writes ctx
awardLoyaltyPoints(ctx); // hook, reads ctx.chargeResult explicitly
return buildReceipt(ctx);
}
protected abstract void chargePayment(OrderContext ctx);
protected void awardLoyaltyPoints(OrderContext ctx) { /* default: no-op */ }
private void validate(OrderContext ctx) { }
private void reserveInventory(OrderContext ctx) { }
private Receipt buildReceipt(OrderContext ctx) { return new Receipt(ctx); }
}
84. Show the design of a PipelineContext class used as the single parameter threaded through every step of a Template Method, and explain what it should and should not contain.
A context object should hold exactly the data steps need to hand off to each other, the raw input, any intermediate results computed so far, and accumulated diagnostics or metadata, all with clear, well-named fields or accessor methods so a step's dependency on a particular piece of context is at least discoverable by reading its body, unlike an implicit instance field.
final class PipelineContext {
private final Object rawInput;
private Object parsedResult;
private final List<String> warnings = new ArrayList<>();
PipelineContext(Object rawInput) { this.rawInput = rawInput; }
Object rawInput() { return rawInput; }
void setParsedResult(Object result) { this.parsedResult = result; }
Object parsedResult() { return parsedResult; }
void addWarning(String warning) { warnings.add(warning); }
}
It should not contain behavior that belongs to a step itself, turning the context into a second, competing place where business logic lives, nor should it hold references to unrelated infrastructure like a database connection, which belongs in the AbstractClass's own fields, injected once, not threaded per-call through every step.
85. Describe a situation where the distinction between an abstract primitive operation and a hook becomes purely nominal, mattering little in practice, and explain why.
When every single existing and reasonably foreseeable ConcreteClass overrides a given step anyway, because the default behavior, while technically valid, is never actually useful in practice, whether that method is declared abstract or given a throwaway default that always gets overridden stops making any real difference to how the hierarchy behaves or how subclass authors experience it.
The distinction still matters formally, abstract enforces the override at compile time while a hook does not, but once a hierarchy is mature and stable with well-understood subclasses, teams often stop debating this classification for existing steps and reserve the abstract-versus-hook judgment call specifically for new steps being added, where the choice still has real compatibility consequences, as discussed for adding new variation points to a live hierarchy.
86. Design a parsing framework's Template Method skeleton: tokenizing raw input is fixed, building the abstract syntax tree from tokens is the one abstract step.
Tokenizing, splitting raw characters into a stream of typed tokens, follows the same lexical rules regardless of what grammar is ultimately being parsed, so it stays fixed. Building the actual syntax tree from that token stream is abstract because a JSON grammar, an arithmetic-expression grammar, and a configuration-file grammar each define entirely different tree shapes and parsing rules.
abstract class Parser<T> {
public final T parse(String input) {
List<Token> tokens = tokenize(input); // fixed
return buildTree(tokens); // abstract, grammar-specific
}
protected abstract T buildTree(List<Token> tokens);
private List<Token> tokenize(String input) { return Lexer.tokenize(input); }
}
87. Compare a SAX-style event callback interface for XML parsing to a Template Method-based parser subclass approach. Why did SAX choose callbacks?
SAX's ContentHandler defines callback methods, startElement(), characters(), endElement(), that the parser invokes as it streams through the document, which is structurally Strategy or Observer, an injected handler object, rather than Template Method's subclassing, because SAX needed the same parser engine to support arbitrarily many independent, swappable handlers for different documents without tying the parsing engine itself to a single fixed class hierarchy.
A Template Method-based parser, by contrast, would require a new subclass per document-handling behavior, and switching what a single parser instance does with the same document at different times would be far more awkward, exactly the composition-versus-inheritance trade-off seen throughout this pattern, made concrete in a real, widely-used JDK API.
88. Explain how java.text.Format's format() and parseObject() methods are structured as a Template Method in the JDK, with subclasses like SimpleDateFormat and DecimalFormat supplying the actual conversion logic.
Format declares the abstract contract that every subclass must fulfill, converting between an object and its textual representation, and provides concrete convenience overloads, such as a no-argument format(Object) built on the more general format(Object, StringBuffer, FieldPosition), that every subclass inherits without reimplementing. SimpleDateFormat and DecimalFormat each implement the actual parsing and formatting logic specific to dates or numbers, while callers can treat any Format subclass identically through the shared, fixed convenience methods.
Format formatter = new SimpleDateFormat("yyyy-MM-dd");
String text = formatter.format(new Date()); // fixed convenience overload, calls the abstract-style core method internally
89. Design a cache-aside layer using Template Method where checking the cache and populating it on a miss are fixed steps, and loading from the underlying source on a cache miss is the one abstract step.
Checking whether a key exists in the cache and, on a miss, storing the freshly loaded value back into the cache before returning it, is identical logic no matter what the underlying data source is, so both are fixed. Loading from the actual source on a miss, a database, a remote API, a file, is abstract because each source's loading logic is completely different.
abstract class CacheAsideLoader<K, V> {
private final Map<K, V> cache = new ConcurrentHashMap<>();
public final V get(K key) {
V cached = cache.get(key); // fixed: check cache
if (cached != null) return cached;
V loaded = loadFromSource(key); // abstract
cache.put(key, loaded); // fixed: populate cache
return loaded;
}
protected abstract V loadFromSource(K key);
}
90. Walk through a bug in the cache-aside Template Method above where loadFromSource() throws partway through, and explain what state the cache is left in and how to fix it.
As written, if loadFromSource() throws, the exception simply propagates and the cache is never populated for that key, which is actually the correct, safe outcome: the fixed step that writes to the cache only runs after the abstract step has already succeeded and returned a value. The real risk appears if a developer "optimizes" by moving the cache write earlier, or by catching and swallowing the exception to store a null or partial placeholder value to "avoid hammering the source again."
public final V get(K key) {
V cached = cache.get(key);
if (cached != null) return cached;
V loaded;
try {
loaded = loadFromSource(key);
} catch (Exception e) {
cache.put(key, null); // BUG: caches a failure as if it were a valid, real value
throw e;
}
cache.put(key, loaded);
return loaded;
}
91. Design a multi-step form validation Template Method where common field checks (required fields, length limits) are fixed and per-form-type business rule validation is abstract, with an optional cross-field validation hook.
Checking that required fields are present and within length limits is identical validation logic regardless of which specific form is being submitted, so it stays fixed. Business rule validation, such as verifying an account number's checksum on a banking form versus verifying an age minimum on a signup form, is abstract because each form type enforces different domain rules. Cross-field validation, checking that a start date precedes an end date, is a hook because many simple forms have no cross-field rules at all.
abstract class FormValidator {
public final ValidationResult validate(FormData form) {
ValidationResult result = checkRequiredFields(form); // fixed
result.merge(checkBusinessRules(form)); // abstract
result.merge(checkCrossFieldRules(form)); // hook, default: no-op
return result;
}
protected abstract ValidationResult checkBusinessRules(FormData form);
protected ValidationResult checkCrossFieldRules(FormData form) { return ValidationResult.empty(); }
private ValidationResult checkRequiredFields(FormData form) { return ValidationResult.empty(); }
}
92. Explain how Java's lack of multiple implementation inheritance limits Template Method when a class would need to combine two independent, unrelated fixed-skeleton behaviors, and describe a workaround.
Because a Java class can extend only one class, a class that needs both an AuditableWorkflow's fixed audit-logging template and a RetryableWorkflow's fixed retry-with-backoff template cannot simply extend both abstract classes to get both skeletons at once, the way it could implement two unrelated interfaces. Choosing one to extend forces the other's behavior to be reimplemented, delegated to, or abandoned.
// Cannot do: class Job extends AuditableWorkflow, RetryableWorkflow { }
// Workaround: compose via delegation instead of inheriting both templates
class Job {
private final AuditableWorkflow audit = new AuditableWorkflow() { /* ... */ };
private final RetryableWorkflow retry = new RetryableWorkflow() { /* ... */ };
public void run() { retry.run(() -> audit.run()); }
}
The workaround is almost always to convert one or both templates into a composed, injected collaborator rather than a base class, which is one more concrete pressure, beyond flexibility and testability, pushing real designs from inheritance-based Template Method toward composition as a hierarchy's requirements grow.
93. Explain how Template Method can be combined with Observer, firing lifecycle events to registered listeners at defined points in the fixed sequence.
The fixed template method can notify a list of registered listener objects at each meaningful boundary, before a step, after a step, on success, on failure, giving external code visibility into or a chance to react to the algorithm's progress without needing to subclass at all. This combines the two patterns cleanly: Template Method still owns the fixed sequence and the mandatory or optional steps, while Observer handles cross-cutting notification to any number of independent listeners.
abstract class ObservableWorkflow {
private final List<WorkflowListener> listeners = new ArrayList<>();
void addListener(WorkflowListener l) { listeners.add(l); }
public final void run() {
listeners.forEach(WorkflowListener::onStart);
doWork(); // abstract
listeners.forEach(WorkflowListener::onComplete);
}
protected abstract void doWork();
}
interface WorkflowListener { void onStart(); void onComplete(); }
94. Design before-export and after-export hook methods around a fixed export step, useful for cross-cutting concerns like logging or metrics without touching the core export logic.
Adding beforeExport() and afterExport() hooks, both defaulting to no-ops, around the core, mandatory export step gives subclasses a place to add timing metrics, structured logging, or a temporary feature-flag check, without needing to touch or duplicate the actual export logic itself, and without forcing every subclass that has no such cross-cutting need to override anything.
abstract class Exporter {
public final void export(Data data) {
beforeExport(data); // hook, default no-op
doExport(data); // abstract, the real work
afterExport(data); // hook, default no-op
}
protected abstract void doExport(Data data);
protected void beforeExport(Data data) { }
protected void afterExport(Data data) { }
}
class MeteredCsvExporter extends Exporter {
@Override protected void doExport(Data data) { /* write CSV */ }
@Override protected void beforeExport(Data data) { Metrics.startTimer("export"); }
@Override protected void afterExport(Data data) { Metrics.stopTimer("export"); }
}
95. Describe how to safely deprecate an existing hook method in a widely-used Template Method base class without immediately breaking any of its subclasses.
Mark the old hook @Deprecated with a Javadoc explanation of what to use instead, but keep its default implementation and keep calling it from the template method exactly as before, so every existing subclass that overrides it continues to compile and behave identically; deprecation is a documented warning, not a removal. Introduce the replacement hook alongside it, and if both exist during a transition period, make the fixed algorithm's behavior consistent regardless of which one, or both, a given subclass has overridden.
/** @deprecated Override {@link #renderBodyV2} instead; this will be removed in a future major version. */
@Deprecated
protected String renderBody(Data data) { return renderBodyV2(data); } // delegates, keeps old overrides working
protected String renderBodyV2(Data data) { return renderBody(data); } // new hook, defaults to calling the old one
Only after a reasonable deprecation window, and ideally a major version bump communicating a breaking-change boundary, should the deprecated hook actually be removed from the class.
96. Explain the specific bug @Override annotations catch when a subclass author intends to override a Template Method step but gets the method signature slightly wrong.
Without @Override, a subclass method with a typo'd name, a slightly different parameter type, or a missing parameter does not override the intended step at all; it simply becomes an unrelated new method that the compiler accepts silently, and the base class's template method keeps calling its own default hook implementation as if the subclass had never overridden anything, with no error or warning anywhere.
abstract class Base { protected boolean validateData(Object parsed) { return true; } }
class Broken extends Base {
protected boolean validateData(String parsed) { return false; } // WRONG parameter type: does NOT override, compiles fine without @Override
}
Adding @Override forces the compiler to verify the method actually overrides a superclass member with a matching signature, turning this exact mistake into an immediate compile error instead of a silent, hard-to-diagnose runtime behavior gap.
97. Explain how Java's sealed classes and interfaces can be used to constrain the set of permitted ConcreteClass subclasses in a Template Method hierarchy, and why this can be valuable.
Declaring the AbstractClass as sealed with an explicit permits clause restricts which classes are allowed to extend it to a known, closed, compiler-enforced list, which is valuable when the set of valid variants is genuinely fixed and finite, such as exactly three supported export formats, and any additional subclass should require a deliberate, visible change to the sealed declaration itself, not a silent addition somewhere else in the codebase.
public sealed abstract class ReportGenerator permits PdfReport, CsvReport, HtmlReport {
public final Report generate(ReportData data) { return new Report(renderBody(data)); }
protected abstract String renderBody(ReportData data);
}
final class PdfReport extends ReportGenerator { protected String renderBody(ReportData data) { return "pdf"; } }
98. Compare implementing variation via a sealed interface with an exhaustive switch expression against classic Template Method's abstract class and subclass overrides. When is each preferable?
A sealed interface with an exhaustive switch expression centralizes all variant-specific logic for a given operation in one place, the switch, making it trivial to see every variant's behavior for that operation side by side, but it means adding a new operation requires touching every existing switch across the codebase, one per operation, rather than adding one new subclass.
| Aspect | Sealed + switch | Template Method subclassing |
|---|---|---|
| Adding a new variant | Update the sealed permits list and every switch | Add one new subclass, others untouched |
| Adding a new operation | Add one new switch; variants stay untouched | Add a new abstract method or hook to the base class |
| Viewing all logic for one variant | Scattered across every switch | Together, in one subclass |
| Viewing all variants for one operation | Together, in one switch | Scattered across every subclass |
This is the classic expression-problem trade-off: prefer sealed plus switch when new operations are added often and the variant set is stable; prefer Template Method subclassing when new variants are added often and the set of operations is stable.
99. Summarize the top three signals that indicate Template Method is genuinely the right pattern for a given design problem, distilled from everything discussed above.
First, the algorithm's overall sequence is genuinely fixed and shared across every variant, with only a small, stable number of steps that differ, not a sequence that itself needs to be reordered or reconfigured per case. Second, the varying steps are deeply coupled to shared internal state that would be awkward to thread through a Strategy interface's parameters, making inheritance's direct access to protected fields a real advantage rather than an accident. Third, nobody needs to swap the variation at runtime on an already-constructed instance; one fixed variant per instance, decided at construction time, is genuinely sufficient for every real use case.
100. Bring together everything covered: write a complete, production-quality ReportGenerator Template Method with a protected constructor, a final template method, one abstract step, one hook with a documented contract, and a working subclass.
This final example deliberately combines every principle discussed across this guide: a protected constructor signaling the class is meant only to be extended, a public final template method guaranteeing the fixed sequence, one abstract step with no sensible default, one hook whose contract is documented via @implSpec, and a guaranteed cleanup step using try/finally.
public abstract class ReportGenerator {
private final ReportConfig config;
protected ReportGenerator(ReportConfig config) { this.config = config; } // protected: extend only
public final Report generate(ReportData data) {
openResources();
try {
String body = renderBody(data); // abstract: no shared default across formats
applyWatermark(body, config); // hook: documented, safe default below
return new Report(renderHeader(), body, renderFooter());
} finally {
closeResources(); // guaranteed cleanup, regardless of failure
}
}
protected abstract String renderBody(ReportData data);
/**
* @implSpec Default implementation applies no watermark. Override to stamp
* a watermark onto {@code body} when {@code config.requiresWatermark()} is true.
*/
protected void applyWatermark(String body, ReportConfig config) { /* default: no watermark */ }
private void openResources() { }
private void closeResources() { }
private String renderHeader() { return "..."; }
private String renderFooter() { return "..."; }
}
final class PdfReportGenerator extends ReportGenerator {
PdfReportGenerator(ReportConfig config) { super(config); }
@Override protected String renderBody(ReportData data) { return PdfRenderer.render(data); }
@Override protected void applyWatermark(String body, ReportConfig config) {
if (config.requiresWatermark()) { PdfRenderer.stamp(body, "CONFIDENTIAL"); }
}
}
Post a Comment
Add