Template Method Pattern
The Template Method pattern defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the algorithmβs structure. The base class controls the flow; subclasses customize individual steps.
Why this matters: When multiple operations share the same high-level workflow but differ in specific steps (data export to different formats, order processing for different merchants, test setup/teardown), Template Method eliminates duplicate orchestration code.
Prerequisites
- SOLID Principles β Template Method supports DIP (subclasses depend on the abstractionβs contract)
- Strategy Pattern β choose between Strategy (composition) vs Template Method (inheritance)
Structure
classDiagram
class OrderProcessor {
+processOrder(order)*
#validateOrder(order)*
#calculateTotal(order)*
#applyDiscount(order)*
#processPayment(order)*
#sendConfirmation(order)*
}
class RestaurantOrderProcessor {
#validateOrder(order)
#calculateTotal(order)
#applyDiscount(order)
#processPayment(order)
#sendConfirmation(order)
}
class GroceryOrderProcessor {
#validateOrder(order)
#calculateTotal(order)
#applyDiscount(order)
#processPayment(order)
#sendConfirmation(order)
}
OrderProcessor <|-- RestaurantOrderProcessor
OrderProcessor <|-- GroceryOrderProcessor
The processOrder() method is final β it defines the algorithm. The protected methods are abstract or have default implementations that subclasses can override.
Real-World Example: Delivery Slot Booking Pipeline
// Template Method: defines the booking flow skeleton
public abstract class DeliverySlotBooker {
// Template method -- final, cannot be overridden
public final BookingResult book(BookingRequest request) {
// Step 1: Validate the request
validate(request);
// Step 2: Check slot availability
List<Slot> available = findAvailableSlots(request);
if (available.isEmpty()) {
return BookingResult.noSlotsAvailable();
}
// Step 3: Apply capacity rules (hook -- optional override)
available = applyCapacityRules(available, request);
// Step 4: Reserve the slot
Slot selected = selectBestSlot(available, request);
Booking booking = reserve(selected, request);
// Step 5: Send confirmation (hook -- default does nothing)
sendConfirmation(booking);
return BookingResult.success(booking);
}
// Abstract steps -- subclasses MUST implement
protected abstract void validate(BookingRequest request);
protected abstract List<Slot> findAvailableSlots(BookingRequest request);
protected abstract Slot selectBestSlot(List<Slot> slots, BookingRequest request);
protected abstract Booking reserve(Slot slot, BookingRequest request);
// Hook methods -- subclasses CAN override (default behavior provided)
protected List<Slot> applyCapacityRules(List<Slot> slots, BookingRequest request) {
return slots; // Default: no filtering
}
protected void sendConfirmation(Booking booking) {
// Default: no-op. Subclasses can override to send email/push/SMS.
}
}
// Concrete: grocery delivery (strict capacity limits)
public class GroceryDeliveryBooker extends DeliverySlotBooker {
@Override
protected void validate(BookingRequest request) {
if (request.getItems().isEmpty()) throw new EmptyCartException();
if (!request.getAddress().isServiceable()) throw new UnserviceableAreaException();
}
@Override
protected List<Slot> findAvailableSlots(BookingRequest request) {
return slotRepository.findByPincodeAndDate(
request.getAddress().getPincode(),
request.getPreferredDate()
);
}
@Override
protected List<Slot> applyCapacityRules(List<Slot> slots, BookingRequest request) {
int cartWeight = calculateWeight(request.getItems());
return slots.stream()
.filter(s -> s.getRemainingCapacity() >= cartWeight)
.filter(s -> s.getOrderCount() < MAX_ORDERS_PER_SLOT)
.toList();
}
@Override
protected Slot selectBestSlot(List<Slot> slots, BookingRequest request) {
return slots.stream()
.min(Comparator.comparingInt(Slot::getOrderCount)) // Least busy
.orElseThrow();
}
@Override
protected Booking reserve(Slot slot, BookingRequest request) {
slot.incrementOrders();
slot.addWeight(calculateWeight(request.getItems()));
return new Booking(request.getUserId(), slot, request.getItems());
}
@Override
protected void sendConfirmation(Booking booking) {
notificationService.sendPush(booking.getUserId(),
"Delivery booked for " + booking.getSlot().getDisplayTime());
}
}
// Concrete: restaurant delivery (time-sensitive, driver-dependent)
public class RestaurantDeliveryBooker extends DeliverySlotBooker {
@Override
protected void validate(BookingRequest request) {
if (!request.getRestaurant().isOpen()) throw new RestaurantClosedException();
}
@Override
protected List<Slot> findAvailableSlots(BookingRequest request) {
int prepTime = request.getRestaurant().getEstimatedPrepTime();
Instant earliest = Instant.now().plusMinutes(prepTime);
return slotRepository.findSlotsAfter(earliest, request.getAddress());
}
@Override
protected Slot selectBestSlot(List<Slot> slots, BookingRequest request) {
// Earliest available slot
return slots.get(0);
}
@Override
protected Booking reserve(Slot slot, BookingRequest request) {
return new Booking(request.getUserId(), slot, request.getItems());
}
}
from abc import ABC, abstractmethod
class DeliverySlotBooker(ABC):
"""Template method: defines the booking algorithm skeleton."""
def book(self, request: 'BookingRequest') -> 'BookingResult':
# Step 1: Validate
self.validate(request)
# Step 2: Find available slots
available = self.find_available_slots(request)
if not available:
return BookingResult.no_slots()
# Step 3: Apply capacity rules (hook)
available = self.apply_capacity_rules(available, request)
# Step 4: Reserve
selected = self.select_best_slot(available, request)
booking = self.reserve(selected, request)
# Step 5: Confirm (hook)
self.send_confirmation(booking)
return BookingResult.success(booking)
@abstractmethod
def validate(self, request): ...
@abstractmethod
def find_available_slots(self, request) -> list: ...
@abstractmethod
def select_best_slot(self, slots: list, request): ...
@abstractmethod
def reserve(self, slot, request) -> 'Booking': ...
# Hooks with default behavior
def apply_capacity_rules(self, slots, request):
return slots # Default: no filtering
def send_confirmation(self, booking):
pass # Default: no-op
class GroceryDeliveryBooker(DeliverySlotBooker):
def validate(self, request):
if not request.items:
raise EmptyCartError()
def find_available_slots(self, request):
return self._slot_repo.find_by_pincode(request.address.pincode, request.date)
def apply_capacity_rules(self, slots, request):
weight = sum(item.weight for item in request.items)
return [s for s in slots if s.remaining_capacity >= weight]
def select_best_slot(self, slots, request):
return min(slots, key=lambda s: s.order_count)
def reserve(self, slot, request):
slot.increment_orders()
return Booking(request.user_id, slot, request.items)
def send_confirmation(self, booking):
send_push(booking.user_id, f"Delivery at {booking.slot.display_time}")
class DeliverySlotBooker {
public:
virtual ~DeliverySlotBooker() = default;
// Template method -- not virtual, controls the flow
BookingResult book(const BookingRequest& request) {
validate(request);
auto available = findAvailableSlots(request);
if (available.empty()) return BookingResult::noSlots();
available = applyCapacityRules(available, request);
auto selected = selectBestSlot(available, request);
auto booking = reserve(selected, request);
sendConfirmation(booking);
return BookingResult::success(booking);
}
protected:
// Abstract steps
virtual void validate(const BookingRequest& req) = 0;
virtual vector<Slot> findAvailableSlots(const BookingRequest& req) = 0;
virtual Slot selectBestSlot(const vector<Slot>& slots, const BookingRequest& req) = 0;
virtual Booking reserve(const Slot& slot, const BookingRequest& req) = 0;
// Hooks
virtual vector<Slot> applyCapacityRules(vector<Slot> slots, const BookingRequest&) {
return slots;
}
virtual void sendConfirmation(const Booking&) { /* no-op */ }
};
class GroceryDeliveryBooker : public DeliverySlotBooker {
protected:
void validate(const BookingRequest& req) override {
if (req.getItems().empty()) throw EmptyCartException();
}
vector<Slot> findAvailableSlots(const BookingRequest& req) override {
return slotRepo_->findByPincode(req.getAddress().getPincode());
}
Slot selectBestSlot(const vector<Slot>& slots, const BookingRequest&) override {
return *min_element(slots.begin(), slots.end(),
[](const Slot& a, const Slot& b) { return a.getOrderCount() < b.getOrderCount(); });
}
Booking reserve(const Slot& slot, const BookingRequest& req) override {
return Booking(req.getUserId(), slot, req.getItems());
}
};
Template Method vs Strategy
| Template Method | Strategy |
|---|---|
| Uses inheritance β subclass overrides steps | Uses composition β inject different algorithms |
| Algorithm skeleton is fixed | Entire algorithm is swappable |
| Subclass IS-A variant of the base class | Strategy HAS-A algorithm |
| Compile-time binding | Runtime binding |
| Use when the overall flow is shared | Use when algorithms are independent |
Rule of thumb: If the high-level flow is the same across variants and only individual steps differ, use Template Method. If the entire algorithm varies, use Strategy.
When to Use vs When to Avoid
| Use Template Method When | Avoid When |
|---|---|
| Multiple algorithms share the same skeleton | Algorithms have completely different structures |
| You want to enforce a specific order of operations | Steps can be called in any order |
| Subclasses only need to customize 2-3 steps | Every step is different (just use Strategy) |
| You want to provide hooks for optional customization | All behavior must be provided |
Interview Questions
-
βHow is Template Method different from Strategy?β β Template Method uses inheritance and defines the skeleton in the base class. Strategy uses composition and lets you swap entire algorithms at runtime. Template Method controls flow; Strategy delegates it.
-
βWhat are hooks?β β Methods with a default (often empty) implementation that subclasses can optionally override. They provide extension points without forcing all subclasses to implement them.
-
βDoesnβt this violate βfavor composition over inheritanceβ?β β Itβs one of the acceptable uses of inheritance: when you genuinely want to share algorithm structure. If the steps are truly independent, prefer Strategy.
-
βWhere do we see this in frameworks?β β JUnitβs
setUp()/tearDown(), servletβsdoGet()/doPost(), Reactβs lifecycle methods, Djangoβs class-based views.