Back to blog

Strategy and Template Method Patterns Explained

oopbehavioral-patternsdesign-patternstypescriptpythonjava
Strategy and Template Method Patterns Explained

Introduction

In the previous post, you learned two structural patterns that wrap objects: Decorator adds behavior, Proxy controls access. Now we enter Phase 5: Behavioral Patterns — patterns that deal with how objects communicate and assign responsibilities.

This post covers two classic behavioral patterns that tame algorithmic complexity:

PatternOne-Line Summary
StrategyEncapsulates a family of algorithms, makes them interchangeable at runtime
Template MethodDefines the skeleton of an algorithm, lets subclasses fill in the details

Both patterns are about algorithms — but they tackle the problem from opposite directions. Strategy selects which algorithm to use. Template Method defines how an algorithm is structured.

What You'll Learn

✅ Replace tangled if/else and switch chains with the Strategy pattern
✅ Select and swap algorithms at runtime without changing client code
✅ Define algorithm skeletons with the Template Method pattern
✅ Use hook methods to give subclasses optional override points
✅ Implement both patterns in TypeScript, Python, and Java
✅ Know when to use Strategy vs Template Method — and when to combine them

Prerequisites


Part 1: The Strategy Pattern

The Problem: Conditional Logic Explosion

You're building a navigation app. Depending on the transport mode, you calculate routes differently:

// ❌ Before Strategy — growing conditional logic
class Navigator {
  buildRoute(origin: string, destination: string, mode: string): void {
    if (mode === "car") {
      console.log(`Building car route: fastest roads from ${origin} to ${destination}`);
      // ... complex car routing logic
    } else if (mode === "bike") {
      console.log(`Building bike route: bike lanes from ${origin} to ${destination}`);
      // ... complex bike routing logic
    } else if (mode === "walk") {
      console.log(`Building walking route: pedestrian paths from ${origin} to ${destination}`);
      // ... complex walking logic
    } else if (mode === "transit") {
      console.log(`Building transit route: buses and trains from ${origin} to ${destination}`);
      // ... complex transit logic
    }
    // Every new transport mode = another else-if branch
  }
}

This class has four problems:

  1. It violates the Open/Closed Principle — you must modify it to add new transport modes
  2. Every new mode inflates the method with more conditional branches
  3. Unit testing requires testing all branches together — not in isolation
  4. The routing logic can't be reused elsewhere independently

The Strategy pattern solves this: extract each algorithm variant into its own class, make them all implement a common interface, and let the client pick which one to use.


Pattern Structure

Participants:

  • Strategy — the interface all algorithm variants implement
  • ConcreteStrategy — a specific algorithm (e.g., CarRouteStrategy, BikeRouteStrategy)
  • Context — holds a reference to the current strategy and delegates algorithm execution to it

Key insight: The Context doesn't care which concrete strategy it holds — it just calls execute(). This means you can swap strategies at runtime without changing any Context code.


TypeScript Implementation

Example 1: Navigation Routes

// Strategy interface — the contract all routing algorithms must fulfill
interface RouteStrategy {
  buildRoute(origin: string, destination: string): string;
}
 
// Concrete strategies — each encapsulates one routing algorithm
class CarRouteStrategy implements RouteStrategy {
  buildRoute(origin: string, destination: string): string {
    return `🚗 Car route: Fastest roads from ${origin} to ${destination} (avoids tolls)`;
  }
}
 
class BikeRouteStrategy implements RouteStrategy {
  buildRoute(origin: string, destination: string): string {
    return `🚴 Bike route: Dedicated bike lanes from ${origin} to ${destination}`;
  }
}
 
class WalkingRouteStrategy implements RouteStrategy {
  buildRoute(origin: string, destination: string): string {
    return `🚶 Walking route: Pedestrian paths from ${origin} to ${destination}`;
  }
}
 
class TransitRouteStrategy implements RouteStrategy {
  buildRoute(origin: string, destination: string): string {
    return `🚌 Transit route: Bus + Metro from ${origin} to ${destination}`;
  }
}
 
// Context — uses whichever strategy the client provides
class Navigator {
  private strategy: RouteStrategy;
 
  constructor(strategy: RouteStrategy) {
    this.strategy = strategy;
  }
 
  // Swap strategy at any time — open/closed
  setStrategy(strategy: RouteStrategy): void {
    this.strategy = strategy;
  }
 
  navigate(origin: string, destination: string): void {
    const route = this.strategy.buildRoute(origin, destination);
    console.log(route);
  }
}
 
// Usage — choose algorithm at runtime
const nav = new Navigator(new CarRouteStrategy());
nav.navigate("Home", "Office");
// 🚗 Car route: Fastest roads from Home to Office (avoids tolls)
 
nav.setStrategy(new BikeRouteStrategy());
nav.navigate("Home", "Office");
// 🚴 Bike route: Dedicated bike lanes from Home to Office
 
nav.setStrategy(new TransitRouteStrategy());
nav.navigate("Home", "Airport");
// 🚌 Transit route: Bus + Metro from Home to Airport

No if/else anywhere in Navigator. Adding a new transport mode is just a new class that implements RouteStrategy — the Navigator stays closed to modification.


Example 2: Payment Processing

A real-world scenario — e-commerce checkout with multiple payment methods:

interface PaymentStrategy {
  pay(amount: number, details: Record<string, string>): boolean;
}
 
class CreditCardStrategy implements PaymentStrategy {
  pay(amount: number, details: Record<string, string>): boolean {
    console.log(`💳 Processing $${amount} via Credit Card ending in ${details.cardNumber?.slice(-4)}`);
    // ... credit card processing logic
    return true;
  }
}
 
class PayPalStrategy implements PaymentStrategy {
  pay(amount: number, details: Record<string, string>): boolean {
    console.log(`🅿️ Processing $${amount} via PayPal account ${details.email}`);
    // ... PayPal API call
    return true;
  }
}
 
class CryptoStrategy implements PaymentStrategy {
  pay(amount: number, details: Record<string, string>): boolean {
    console.log(`₿ Processing $${amount} worth of ${details.currency} to wallet ${details.walletAddress}`);
    // ... blockchain transaction
    return true;
  }
}
 
class BuyNowPayLaterStrategy implements PaymentStrategy {
  pay(amount: number, details: Record<string, string>): boolean {
    console.log(`📅 Split $${amount} into 4 installments via ${details.provider}`);
    return true;
  }
}
 
class ShoppingCart {
  private items: { name: string; price: number }[] = [];
  private paymentStrategy: PaymentStrategy;
 
  constructor(paymentStrategy: PaymentStrategy) {
    this.paymentStrategy = paymentStrategy;
  }
 
  setPaymentStrategy(strategy: PaymentStrategy): void {
    this.paymentStrategy = strategy;
  }
 
  addItem(name: string, price: number): void {
    this.items.push({ name, price });
  }
 
  checkout(details: Record<string, string>): void {
    const total = this.items.reduce((sum, item) => sum + item.price, 0);
    console.log(`\nOrder total: $${total}`);
    const success = this.paymentStrategy.pay(total, details);
    if (success) {
      console.log("✅ Payment successful!\n");
    }
  }
}
 
// Usage
const cart = new ShoppingCart(new CreditCardStrategy());
cart.addItem("Laptop", 999);
cart.addItem("Mouse", 49);
cart.checkout({ cardNumber: "4111111111112345" });
 
// User switches to PayPal at checkout
cart.setPaymentStrategy(new PayPalStrategy());
cart.checkout({ email: "alice@example.com" });

Example 3: Sort Strategy

interface SortStrategy<T> {
  sort(data: T[]): T[];
}
 
class BubbleSortStrategy<T> implements SortStrategy<T> {
  sort(data: T[]): T[] {
    const arr = [...data];
    for (let i = 0; i < arr.length - 1; i++) {
      for (let j = 0; j < arr.length - i - 1; j++) {
        if (arr[j] > arr[j + 1]) {
          [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        }
      }
    }
    return arr;
  }
}
 
class QuickSortStrategy<T> implements SortStrategy<T> {
  sort(data: T[]): T[] {
    if (data.length <= 1) return data;
    const pivot = data[Math.floor(data.length / 2)];
    const left = data.filter(x => x < pivot);
    const middle = data.filter(x => x === pivot);
    const right = data.filter(x => x > pivot);
    return [...this.sort(left), ...middle, ...this.sort(right)];
  }
}
 
class Sorter<T> {
  constructor(private strategy: SortStrategy<T>) {}
 
  setStrategy(strategy: SortStrategy<T>): void {
    this.strategy = strategy;
  }
 
  sort(data: T[]): T[] {
    return this.strategy.sort(data);
  }
}
 
// Choose algorithm based on dataset size
const sorter = new Sorter<number>(new BubbleSortStrategy());
const smallDataset = [5, 2, 8, 1, 9];
console.log(sorter.sort(smallDataset)); // [1, 2, 5, 8, 9]
 
sorter.setStrategy(new QuickSortStrategy());
const largeDataset = Array.from({ length: 1000 }, () => Math.random() * 1000);
sorter.sort(largeDataset); // QuickSort handles large datasets efficiently

Python Implementation

Python's first-class functions let you implement Strategy even more concisely — either with classes or with plain functions:

from abc import ABC, abstractmethod
from typing import Callable
 
# --- Class-based Strategy (same as TypeScript) ---
 
class CompressionStrategy(ABC):
    @abstractmethod
    def compress(self, filename: str) -> str:
        pass
 
class ZipStrategy(CompressionStrategy):
    def compress(self, filename: str) -> str:
        return f"Compressing {filename} using ZIP format (good compression ratio)"
 
class RarStrategy(CompressionStrategy):
    def compress(self, filename: str) -> str:
        return f"Compressing {filename} using RAR format (smaller archives)"
 
class TarGzStrategy(CompressionStrategy):
    def compress(self, filename: str) -> str:
        return f"Compressing {filename} using TAR.GZ format (Unix/Linux standard)"
 
class Archiver:
    def __init__(self, strategy: CompressionStrategy):
        self._strategy = strategy
 
    def set_strategy(self, strategy: CompressionStrategy) -> None:
        self._strategy = strategy
 
    def archive(self, filename: str) -> str:
        return self._strategy.compress(filename)
 
# Class-based usage
archiver = Archiver(ZipStrategy())
print(archiver.archive("documents.tar"))
# Compressing documents.tar using ZIP format (good compression ratio)
 
archiver.set_strategy(TarGzStrategy())
print(archiver.archive("backup.tar"))
# Compressing backup.tar using TAR.GZ format (Unix/Linux standard)
 
 
# --- Function-based Strategy (Pythonic shortcut) ---
# When the algorithm is simple, use callables directly
 
StrategyFn = Callable[[str], str]
 
def zip_compress(filename: str) -> str:
    return f"ZIP: {filename}.zip"
 
def rar_compress(filename: str) -> str:
    return f"RAR: {filename}.rar"
 
class FunctionalArchiver:
    def __init__(self, strategy: StrategyFn):
        self._strategy = strategy
 
    def archive(self, filename: str) -> str:
        return self._strategy(filename)
 
fa = FunctionalArchiver(zip_compress)
print(fa.archive("report"))  # ZIP: report.zip
 
fa._strategy = rar_compress
print(fa.archive("report"))  # RAR: report.rar
 
# Even simpler: use a lambda
fa._strategy = lambda f: f"GZIP: {f}.gz"
print(fa.archive("report"))  # GZIP: report.gz

Java Implementation

// Strategy Pattern — Discount calculation in an e-commerce system
 
// Strategy interface
public interface DiscountStrategy {
    double calculateDiscount(double originalPrice);
    String getDescription();
}
 
// Concrete strategies
public class NoDiscountStrategy implements DiscountStrategy {
    @Override
    public double calculateDiscount(double originalPrice) {
        return 0;
    }
 
    @Override
    public String getDescription() {
        return "No discount applied";
    }
}
 
public class PercentageDiscountStrategy implements DiscountStrategy {
    private final double percentage;
 
    public PercentageDiscountStrategy(double percentage) {
        this.percentage = percentage;
    }
 
    @Override
    public double calculateDiscount(double originalPrice) {
        return originalPrice * (percentage / 100);
    }
 
    @Override
    public String getDescription() {
        return String.format("%.0f%% off", percentage);
    }
}
 
public class FixedAmountDiscountStrategy implements DiscountStrategy {
    private final double amount;
 
    public FixedAmountDiscountStrategy(double amount) {
        this.amount = amount;
    }
 
    @Override
    public double calculateDiscount(double originalPrice) {
        return Math.min(amount, originalPrice);
    }
 
    @Override
    public String getDescription() {
        return String.format("$%.2f off", amount);
    }
}
 
public class BuyOneGetOneFreeStrategy implements DiscountStrategy {
    @Override
    public double calculateDiscount(double originalPrice) {
        return originalPrice / 2; // 50% effective discount
    }
 
    @Override
    public String getDescription() {
        return "Buy 1, Get 1 Free";
    }
}
 
// Context
public class PricingEngine {
    private DiscountStrategy strategy;
 
    public PricingEngine(DiscountStrategy strategy) {
        this.strategy = strategy;
    }
 
    public void setStrategy(DiscountStrategy strategy) {
        this.strategy = strategy;
    }
 
    public double getFinalPrice(double originalPrice) {
        double discount = strategy.calculateDiscount(originalPrice);
        double finalPrice = originalPrice - discount;
        System.out.printf("Original: $%.2f | %s | Final: $%.2f%n",
                originalPrice, strategy.getDescription(), finalPrice);
        return finalPrice;
    }
}
 
// Usage
public class Main {
    public static void main(String[] args) {
        PricingEngine engine = new PricingEngine(new NoDiscountStrategy());
        engine.getFinalPrice(100.0);
        // Original: $100.00 | No discount applied | Final: $100.00
 
        engine.setStrategy(new PercentageDiscountStrategy(20));
        engine.getFinalPrice(100.0);
        // Original: $100.00 | 20% off | Final: $80.00
 
        engine.setStrategy(new FixedAmountDiscountStrategy(15));
        engine.getFinalPrice(100.0);
        // Original: $100.00 | $15.00 off | Final: $85.00
 
        engine.setStrategy(new BuyOneGetOneFreeStrategy());
        engine.getFinalPrice(100.0);
        // Original: $100.00 | Buy 1, Get 1 Free | Final: $50.00
    }
}

Part 2: The Template Method Pattern

The Problem: Algorithm Duplication

You're building a data mining application. It reads files in different formats (CSV, JSON, XML) and generates reports. The overall process is always the same:

1. Open file  →  2. Extract data  →  3. Parse data  →  4. Analyze data  →  5. Generate report  →  6. Close file

But steps 2 and 3 differ per format. Without Template Method, you'd have three classes that each copy-paste the same structure:

// ❌ Duplication — identical structure, different inner steps
class CSVDataMiner {
  mine(filePath: string): void {
    this.openFile(filePath);   // same
    this.extractCSVData();     // different
    this.parseCSVData();       // different
    this.analyzeData();        // same
    this.generateReport();     // same
    this.closeFile();          // same
  }
  // ...
}
 
class JSONDataMiner {
  mine(filePath: string): void {
    this.openFile(filePath);   // same (duplicate)
    this.extractJSONData();    // different
    this.parseJSONData();      // different
    this.analyzeData();        // same (duplicate)
    this.generateReport();     // same (duplicate)
    this.closeFile();          // same (duplicate)
  }
  // ...
}

Four out of six steps are identical — but you're duplicating them in every class. If analyzeData ever changes, you update three places.

The Template Method pattern solves this: put the algorithm skeleton in a base class and let subclasses override only the steps that vary.


Pattern Structure

Participants:

  • AbstractClass — defines the templateMethod() which calls all steps in order; implements the invariant steps; declares abstract methods for variant steps; optionally provides hook methods
  • ConcreteClass — implements the abstract steps that vary; may optionally override hook methods

Two types of methods in the base class:

  • Abstract methods — must be overridden in subclasses (the variable parts)
  • Hook methods — have a default (empty or minimal) implementation; subclasses may override them (optional customization points)

TypeScript Implementation

Example 1: Data Mining Pipeline

// Abstract base — defines the algorithm skeleton
abstract class DataMiner {
  // The template method — final: subclasses cannot change the order
  public mine(filePath: string): void {
    this.openFile(filePath);   // invariant: same for all formats
    this.extractData();        // variant: must be overridden
    this.parseData();          // variant: must be overridden
    this.analyzeData();        // invariant: same for all
    this.generateReport();     // invariant: same for all
    this.closeFile();          // invariant: same for all
  }
 
  // Invariant steps — implemented here, not overridden
  private openFile(filePath: string): void {
    console.log(`📂 Opening file: ${filePath}`);
  }
 
  private analyzeData(): void {
    console.log("📊 Analyzing extracted data...");
  }
 
  private generateReport(): void {
    console.log("📄 Generating report...");
  }
 
  private closeFile(): void {
    console.log("🔒 Closing file.");
  }
 
  // Variant steps — subclasses MUST implement these
  protected abstract extractData(): void;
  protected abstract parseData(): void;
 
  // Hook method — optional override, empty default
  protected afterParse(): void {
    // Subclasses can override to add post-parse behavior
  }
}
 
// Concrete class — only implements what varies
class CSVDataMiner extends DataMiner {
  protected extractData(): void {
    console.log("📋 Extracting rows from CSV file");
  }
 
  protected parseData(): void {
    console.log("🔍 Parsing CSV: splitting by commas, mapping headers");
  }
}
 
class JSONDataMiner extends DataMiner {
  protected extractData(): void {
    console.log("📋 Extracting JSON objects from file");
  }
 
  protected parseData(): void {
    console.log("🔍 Parsing JSON: deserializing nested objects");
  }
 
  // This subclass uses the optional hook
  protected afterParse(): void {
    console.log("🔗 JSON: Resolving $ref references...");
  }
}
 
class XMLDataMiner extends DataMiner {
  protected extractData(): void {
    console.log("📋 Extracting XML nodes from file");
  }
 
  protected parseData(): void {
    console.log("🔍 Parsing XML: building DOM tree");
  }
}
 
// Usage — same interface, different inner behavior
console.log("=== CSV Mining ===");
new CSVDataMiner().mine("sales_2026.csv");
 
console.log("\n=== JSON Mining ===");
new JSONDataMiner().mine("products.json");
 
console.log("\n=== XML Mining ===");
new XMLDataMiner().mine("config.xml");

Output:

=== CSV Mining ===
📂 Opening file: sales_2026.csv
📋 Extracting rows from CSV file
🔍 Parsing CSV: splitting by commas, mapping headers
📊 Analyzing extracted data...
📄 Generating report...
🔒 Closing file.
 
=== JSON Mining ===
📂 Opening file: products.json
📋 Extracting JSON objects from file
🔍 Parsing JSON: deserializing nested objects
📊 Analyzing extracted data...
📄 Generating report...
🔒 Closing file.

Example 2: Beverage Preparation

The classic Gang of Four example — making tea vs. coffee:

abstract class HotBeverage {
  // Template method — the fixed recipe
  public prepare(): void {
    this.boilWater();
    this.brew();
    this.pourInCup();
    if (this.customerWantsCondiments()) {  // hook controls optional step
      this.addCondiments();
    }
  }
 
  // Fixed steps
  private boilWater(): void {
    console.log("🔥 Boiling water");
  }
 
  private pourInCup(): void {
    console.log("☕ Pouring into cup");
  }
 
  // Variant steps — subclasses fill in
  protected abstract brew(): void;
  protected abstract addCondiments(): void;
 
  // Hook — subclasses can override to say "no condiments, please"
  protected customerWantsCondiments(): boolean {
    return true; // default: yes
  }
}
 
class Tea extends HotBeverage {
  protected brew(): void {
    console.log("🍃 Steeping the tea bag (3 minutes)");
  }
 
  protected addCondiments(): void {
    console.log("🍋 Adding lemon slice");
  }
}
 
class Coffee extends HotBeverage {
  protected brew(): void {
    console.log("☕ Dripping coffee through filter");
  }
 
  protected addCondiments(): void {
    console.log("🥛 Adding sugar and milk");
  }
}
 
class BlackCoffee extends HotBeverage {
  protected brew(): void {
    console.log("☕ Dripping coffee through filter");
  }
 
  protected addCondiments(): void {
    // Never called anyway
  }
 
  // Override hook — no condiments for black coffee
  protected customerWantsCondiments(): boolean {
    return false;
  }
}
 
console.log("--- Making Tea ---");
new Tea().prepare();
 
console.log("\n--- Making Coffee ---");
new Coffee().prepare();
 
console.log("\n--- Making Black Coffee ---");
new BlackCoffee().prepare();
// No "Adding" step — hook returned false

Example 3: Order Processing Workflow

abstract class OrderProcessor {
  // Template method — processing pipeline
  public processOrder(orderId: string): void {
    console.log(`\n🛒 Processing order #${orderId}`);
    if (!this.validateOrder(orderId)) {
      console.log("❌ Order validation failed. Aborting.");
      return;
    }
    this.processPayment(orderId);
    this.fulfillOrder(orderId);
    this.sendNotification(orderId);
    this.logOrder(orderId);         // invariant hook behavior
  }
 
  // Abstract — each channel processes differently
  protected abstract validateOrder(orderId: string): boolean;
  protected abstract processPayment(orderId: string): void;
  protected abstract fulfillOrder(orderId: string): void;
 
  // Invariant steps (same for all channels)
  private sendNotification(orderId: string): void {
    console.log(`📧 Confirmation email sent for order #${orderId}`);
  }
 
  private logOrder(orderId: string): void {
    console.log(`📝 Order #${orderId} logged to audit trail`);
  }
}
 
class OnlineOrderProcessor extends OrderProcessor {
  protected validateOrder(orderId: string): boolean {
    console.log("🔍 Validating online order: checking inventory + fraud detection");
    return true;
  }
 
  protected processPayment(orderId: string): void {
    console.log("💳 Processing online payment via Stripe");
  }
 
  protected fulfillOrder(orderId: string): void {
    console.log("📦 Creating shipment label, scheduling courier pickup");
  }
}
 
class InStoreOrderProcessor extends OrderProcessor {
  protected validateOrder(orderId: string): boolean {
    console.log("🔍 Validating in-store order: checking shelf availability");
    return true;
  }
 
  protected processPayment(orderId: string): void {
    console.log("🏪 Processing POS terminal payment");
  }
 
  protected fulfillOrder(orderId: string): void {
    console.log("🛍️ Order ready for immediate pickup");
  }
}
 
new OnlineOrderProcessor().processOrder("ORD-1001");
new InStoreOrderProcessor().processOrder("ORD-1002");

Python Implementation

from abc import ABC, abstractmethod
 
class ReportGenerator(ABC):
    """Abstract base defining the report generation algorithm."""
 
    # Template method — sealed (Python doesn't enforce, but conventionally final)
    def generate(self, data: dict) -> str:
        header = self._create_header(data)
        body = self._create_body(data)
        footer = self._create_footer()
        extras = self._add_extras(data)   # hook
        return "\n".join(filter(None, [header, body, extras, footer]))
 
    # Invariant steps
    def _create_footer(self) -> str:
        return "--- End of Report ---"
 
    # Abstract steps — subclasses must implement
    @abstractmethod
    def _create_header(self, data: dict) -> str:
        pass
 
    @abstractmethod
    def _create_body(self, data: dict) -> str:
        pass
 
    # Hook — optional enhancement
    def _add_extras(self, data: dict) -> str:
        return ""  # Default: nothing extra
 
 
class PDFReportGenerator(ReportGenerator):
    def _create_header(self, data: dict) -> str:
        return f"[PDF] REPORT: {data['title'].upper()}\nGenerated: {data['date']}"
 
    def _create_body(self, data: dict) -> str:
        lines = [f"  • {k}: {v}" for k, v in data.get("metrics", {}).items()]
        return "[PDF Body]\n" + "\n".join(lines)
 
    def _add_extras(self, data: dict) -> str:
        return "[PDF] Embedded charts and graphs attached"
 
 
class HTMLReportGenerator(ReportGenerator):
    def _create_header(self, data: dict) -> str:
        return f"<h1>{data['title']}</h1><p>Date: {data['date']}</p>"
 
    def _create_body(self, data: dict) -> str:
        rows = "".join(f"<tr><td>{k}</td><td>{v}</td></tr>"
                       for k, v in data.get("metrics", {}).items())
        return f"<table>{rows}</table>"
 
 
class MarkdownReportGenerator(ReportGenerator):
    def _create_header(self, data: dict) -> str:
        return f"# {data['title']}\n**Date:** {data['date']}"
 
    def _create_body(self, data: dict) -> str:
        lines = [f"| {k} | {v} |" for k, v in data.get("metrics", {}).items()]
        return "| Metric | Value |\n|--------|-------|\n" + "\n".join(lines)
 
 
# Usage — same template, different output formats
report_data = {
    "title": "Monthly Sales Report",
    "date": "2026-03-05",
    "metrics": {
        "Revenue": "$45,200",
        "Orders": 312,
        "New Customers": 87,
    }
}
 
generators = [PDFReportGenerator(), HTMLReportGenerator(), MarkdownReportGenerator()]
 
for generator in generators:
    print(f"\n{'='*40}")
    print(generator.generate(report_data))

Java Implementation

// Template Method — Game lifecycle skeleton
 
public abstract class Game {
    // Template method — final ensures subclasses can't change game loop order
    public final void play() {
        initialize();
        startPlay();
        while (!isDone()) {
            takeTurn();
        }
        endPlay();
        announceWinner();
    }
 
    // Abstract steps — subclasses must implement game-specific behavior
    protected abstract void initialize();
    protected abstract void startPlay();
    protected abstract void takeTurn();
    protected abstract boolean isDone();
    protected abstract void endPlay();
 
    // Invariant step — announcing winner is always the same structure
    private void announceWinner() {
        System.out.println("🏆 Game over! " + getWinnerName() + " wins!");
    }
 
    // Hook — subclass provides winner name
    protected String getWinnerName() {
        return "Player 1"; // default
    }
}
 
public class Chess extends Game {
    private int turnCount = 0;
 
    @Override
    protected void initialize() {
        System.out.println("♟️ Chess: Setting up the board with 32 pieces");
    }
 
    @Override
    protected void startPlay() {
        System.out.println("♟️ Chess: White moves first");
    }
 
    @Override
    protected void takeTurn() {
        turnCount++;
        System.out.println("♟️ Chess: Turn " + turnCount + " — player makes a move");
    }
 
    @Override
    protected boolean isDone() {
        return turnCount >= 3; // simplified: 3 turns for demo
    }
 
    @Override
    protected void endPlay() {
        System.out.println("♟️ Chess: Checkmate detected");
    }
 
    @Override
    protected String getWinnerName() {
        return "White Player";
    }
}
 
public class TicTacToe extends Game {
    private int moveCount = 0;
 
    @Override
    protected void initialize() {
        System.out.println("❌ TicTacToe: Drawing 3x3 grid");
    }
 
    @Override
    protected void startPlay() {
        System.out.println("❌ TicTacToe: X goes first");
    }
 
    @Override
    protected void takeTurn() {
        moveCount++;
        System.out.println("❌ TicTacToe: Move " + moveCount);
    }
 
    @Override
    protected boolean isDone() {
        return moveCount >= 3;
    }
 
    @Override
    protected void endPlay() {
        System.out.println("❌ TicTacToe: Three in a row!");
    }
}
 
// Main
Game chess = new Chess();
chess.play();
 
System.out.println();
 
Game ttt = new TicTacToe();
ttt.play();

Strategy vs Template Method — Side by Side

These two patterns both deal with algorithms, but they operate differently:

DimensionStrategyTemplate Method
MechanismObject composition (holds a strategy object)Class inheritance (extends the base class)
Algorithm selectionAt runtime — swap the strategy objectAt compile time — chosen by subclass
Varies byThe entire algorithmParts (steps) of the algorithm
Control flowClient drives it — calls context.execute()Base class drives it — calls template method
Open/ClosedAdd algorithms by adding new strategy classesAdd variants by creating new subclasses
FlexibilityMore flexible — strategy can change at runtimeLess flexible — locked to one subclass
Hollywood PrincipleNo — context calls strategyYes — "Don't call us, we'll call you"
Best forDifferent algorithms with same intentSame algorithm flow, different step details

The key question: Can you swap the behavior at runtime after the object is created?

  • Yes → Strategy (inject a different strategy object)
  • No, it's baked in at class selection time → Template Method (use a different subclass)

Combining Strategy with Template Method

In real systems, these patterns are often used together. The Template Method defines the overall workflow; individual steps delegate to Strategy objects for fine-grained algorithm selection:

// Template Method defines the order
// Strategy handles the variable sub-algorithms
 
interface SortAlgorithm {
  sort<T>(data: T[]): T[];
}
 
abstract class DataProcessor {
  constructor(protected sortStrategy: SortAlgorithm) {}
 
  // Template method — processing pipeline
  public process(data: number[]): void {
    const validated = this.validate(data);           // abstract
    const sorted = this.sortStrategy.sort(validated); // Strategy!
    const result = this.transform(sorted);           // abstract
    this.output(result);                             // invariant
  }
 
  protected abstract validate(data: number[]): number[];
  protected abstract transform(data: number[]): number[];
 
  private output(data: number[]): void {
    console.log("Result:", data.join(", "));
  }
}
 
class FilterProcessor extends DataProcessor {
  protected validate(data: number[]): number[] {
    return data.filter(n => n > 0); // strip negatives
  }
 
  protected transform(data: number[]): number[] {
    return data.map(n => n * 2); // double each value
  }
}
 
// Usage
const processor = new FilterProcessor(new QuickSortStrategy());
processor.process([5, -1, 3, 0, 8, -2, 1]);
// Result: 2, 6, 10, 16

Real-World Use Cases

Where You'll Find Strategy in the Wild

Authentication systems:

interface AuthStrategy {
  authenticate(credentials: Record<string, string>): Promise<boolean>;
}
// JWTStrategy, OAuth2Strategy, APIKeyStrategy, BasicAuthStrategy

Logging backends:

interface LogStrategy {
  log(level: string, message: string): void;
}
// ConsoleLogStrategy, FileLogStrategy, ElasticSearchStrategy, DatadogStrategy

Caching policies:

interface CacheStrategy {
  get(key: string): any;
  set(key: string, value: any, ttl: number): void;
}
// InMemoryCache, RedisCache, NoOpCache (for testing)

File storage:

interface StorageStrategy {
  upload(file: Buffer, path: string): Promise<string>;
}
// LocalStorageStrategy, S3Strategy, AzureBlobStrategy, GCSStrategy

Where You'll Find Template Method in the Wild

Spring FrameworkAbstractController, JdbcTemplate, HibernateTemplate all use Template Method. The framework defines the workflow (transaction begin → execute → commit/rollback), you override only the query/update step.

JUnit test lifecycle@BeforeEach, @Test, @AfterEach is a Template Method: the test runner drives the setup → test → teardown sequence.

React component lifecyclecomponentDidMount, componentDidUpdate, componentWillUnmount hooks are Template Method: React drives the lifecycle, you override what you need.

Angular HTTP Interceptors — define a template: each request goes through intercept → transform → next.handle. You implement the transform step.


Common Pitfalls

Strategy Pitfalls

Pitfall 1: Bloating the strategy interface

// ❌ Forcing all strategies to implement everything
interface OverloadedStrategy {
  sort(data: any[]): any[];
  validate(data: any[]): boolean;
  transform(data: any[]): any[];
  // ...
}
 
// ✅ Keep strategy interface focused on ONE algorithm
interface SortStrategy {
  sort<T>(data: T[]): T[];
}

Pitfall 2: Using Strategy when a simple function suffices

// ❌ Overkill for simple one-method operations
class AddTaxStrategy implements PriceStrategy {
  calculate(price: number): number { return price * 1.1; }
}
 
// ✅ Just pass a function
function applyPricing(price: number, strategy: (p: number) => number) {
  return strategy(price);
}
applyPricing(100, price => price * 1.1);

Pitfall 3: Allowing null strategies

// ❌ Crashes at runtime if strategy not set
class Context {
  private strategy?: Strategy;  // Could be undefined!
  execute() { this.strategy!.run(); }
}
 
// ✅ Use Null Object Pattern or enforce via constructor
class Context {
  constructor(private strategy: Strategy) {}  // Required at construction
  execute() { this.strategy.run(); }
}

Template Method Pitfalls

Pitfall 1: Exposing too many abstract methods

// ❌ Too many abstract steps = too much burden on subclasses
abstract class Bloated {
  template() { this.a(); this.b(); this.c(); this.d(); this.e(); }
  abstract a(): void;
  abstract b(): void;
  abstract c(): void;
  abstract d(): void;
  abstract e(): void;
}
 
// ✅ Keep abstract methods minimal; make others hooks with defaults
abstract class Lean {
  template() { this.a(); this.b(); this.c(); }
  abstract a(): void;     // must override
  protected b(): void {}  // optional hook
  private c(): void { console.log("invariant"); } // always same
}

Pitfall 2: Calling abstract methods in the constructor

// ❌ Dangerous — subclass fields not initialized yet in Java/TypeScript
abstract class Bad {
  constructor() {
    this.init(); // Subclass init() may reference uninitialized fields!
  }
  abstract init(): void;
}
 
// ✅ Call abstract methods from the template method, not constructor
abstract class Good {
  public run(): void {
    this.init(); // Called after construction — safe
    this.execute();
  }
  protected abstract init(): void;
  protected abstract execute(): void;
}

Pitfall 3: Overriding the template method itself

The whole point of Template Method is that the skeleton is invariant. If subclasses override the template method, they break the contract.

// ✅ Mark template methods as final (Java) or use naming convention (TypeScript)
abstract class AbstractProcessor {
  // "final" in Java; in TypeScript, use convention + linting
  public readonly process = (data: string): void => {
    this.preProcess(data);
    this.execute(data);
    this.postProcess(data);
  };
 
  protected abstract execute(data: string): void;
  protected preProcess(data: string): void {}  // hook
  protected postProcess(data: string): void {} // hook
}

Summary and Key Takeaways

Both Strategy and Template Method are behavioral patterns for managing algorithmic variation — but each has its place.

Strategy Pattern — Composition-based algorithm selection:

✅ Extracts algorithm variants into separate classes behind a shared interface
✅ Enables runtime algorithm swapping without touching the context
✅ Eliminates conditional branching (if/else, switch on type)
✅ Each strategy is independently testable
✅ New algorithms added without modifying existing code (Open/Closed)

Template Method Pattern — Inheritance-based algorithm skeleton:

✅ Defines the algorithm structure once in the base class
✅ Subclasses override only the variant steps — no duplication
✅ Hook methods provide optional customization points
✅ The Hollywood Principle: base class drives the flow, subclasses fill in details
✅ Guarantees the algorithm executes in the correct order

Decision guide:

  • Will you need to change the algorithm at runtime? → Strategy
  • Is the algorithm structure fixed, only inner steps vary? → Template Method
  • Need both? → Combine them: Template Method for the skeleton, Strategy for the variable steps

What's Next

We have one post left in this series — the behavioral patterns trifecta:

➡️ Next: Observer, Command, and State Patterns — event-driven behavior, undo/redo, and state machines. After that, the OOP & Design Patterns series is complete!

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.