Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
⏱️ 7 min read

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


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

  1. β€œ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.

  2. β€œ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.

  3. β€œ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.

  4. β€œWhere do we see this in frameworks?” – JUnit’s setUp()/tearDown(), servlet’s doGet()/doPost(), React’s lifecycle methods, Django’s class-based views.


See It in Action

Delivery Slot Booking

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access