Back to Blog
design-patternscsharptypescriptbehavioral-patterns

Template Method Pattern — FixIt Pro Series #21

Every FixIt Pro inspection follows the same sequence, but the steps differ per trade. Learn how the Template Method locks the algorithm structure while letting subclasses fill in the details, in C# and TypeScript.

Series: Design Patterns with FixIt Pro  ·  Episode 21 / 22  ·  Behavioral Pattern
Previous: #20 — Strategy Pattern


The Scenario

Every FixIt Pro job ends with an inspection report. The report always follows the same sequence:

  1. Open the report — record job ID, inspector, timestamp
  2. Verify site safety — trade-specific safety checks
  3. Perform the inspection — trade-specific inspection steps
  4. Record findings — trade-specific findings and measurements
  5. Attach compliance note — trade-specific regulatory reference
  6. Sign off — inspector signature and closing timestamp

Steps 1 and 6 are identical for every trade. Steps 2 to 5 differ completely between plumbing, electrical, and carpentry.

The order never changes. Skipping step 2 before step 3 isn't allowed. The sign-off must always come last. This is a fixed algorithm with variable steps.

Copy-pasting the whole sequence into PlumbingInspection, ElectricalInspection, and CarpentryInspection means three copies of the ordering logic. Change the sequence, lets say, add a photo-capture step between findings and compliance, and you edit three files and hope you didn't miss one.

The Template Method puts the sequence in one place and lets subclasses fill in only the parts that vary.


What Is the Template Method Pattern?

Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.

One method, the template method, calls a fixed sequence of steps. Some steps are implemented in the base class. Some are abstract and must be implemented by subclasses. Some are hooks, optional steps with a default empty implementation that subclasses may override.

The two participants

Role FixIt Pro equivalent
Abstract Class JobInspectionReport — defines the template method and the step contracts
Concrete Classes PlumbingInspection, ElectricalInspection, CarpentryInspection

Three kinds of steps

Step type Meaning FixIt Pro example
Concrete step Implemented in base, same for all OpenReport(), SignOff()
Abstract step Must be implemented by subclass VerifySafety(), PerformInspection()
Hook Optional, empty default in base CaptureAdditionalPhotos()

C# Implementation

// ── Abstract Class — defines the template method ───────────

public abstract class JobInspectionReport
{
    protected string JobId       { get; private set; } = string.Empty;
    protected string Inspector   { get; private set; } = string.Empty;
    protected readonly List<string> Findings = new();

    // ══════════════════════════════════════════════════════
    // THE TEMPLATE METHOD
    // sealed — subclasses cannot change the sequence,
    // only the individual steps.
    // ══════════════════════════════════════════════════════
    public void GenerateReport(string jobId, string inspector)
    {
        JobId     = jobId;
        Inspector = inspector;
        Findings.Clear();

        OpenReport();                 // concrete — same for all
        VerifySafety();               // abstract — subclass must implement
        PerformInspection();          // abstract — subclass must implement
        RecordFindings();             // abstract — subclass must implement
        CaptureAdditionalPhotos();    // hook     — optional override
        AttachComplianceNote();       // abstract — subclass must implement
        SignOff();                    // concrete — same for all
    }

    // ── Concrete steps — identical for every trade ─────────

    private void OpenReport()
    {
        Console.WriteLine($"\n╔══════════════════════════════════════════════════");
        Console.WriteLine($"║ INSPECTION REPORT — {TradeName}");
        Console.WriteLine($"╠══════════════════════════════════════════════════");
        Console.WriteLine($"║ Job ID    : #{JobId}");
        Console.WriteLine($"║ Inspector : {Inspector}");
        Console.WriteLine($"║ Opened    : {DateTime.Now:dd MMM yyyy HH:mm}");
        Console.WriteLine($"╠══════════════════════════════════════════════════");
    }

    private void SignOff()
    {
        Console.WriteLine($"╠══════════════════════════════════════════════════");
        Console.WriteLine($"║ Findings recorded : {Findings.Count}");
        Console.WriteLine($"║ Signed off by     : {Inspector}");
        Console.WriteLine($"║ Closed            : {DateTime.Now:dd MMM yyyy HH:mm}");
        Console.WriteLine($"╚══════════════════════════════════════════════════");
    }

    // ── Abstract steps — subclasses MUST implement ─────────

    protected abstract string TradeName { get; }
    protected abstract void   VerifySafety();
    protected abstract void   PerformInspection();
    protected abstract void   RecordFindings();
    protected abstract void   AttachComplianceNote();

    // ── Hook — optional, empty by default ──────────────────

    protected virtual void CaptureAdditionalPhotos()
    {
        // Default: no additional photos required
    }

    // ── Helper available to all subclasses ─────────────────

    protected void AddFinding(string finding)
    {
        Findings.Add(finding);
        Console.WriteLine($"║   • {finding}");
    }
}

// ── Concrete Class — Plumbing ──────────────────────────────

public class PlumbingInspection : JobInspectionReport
{
    protected override string TradeName => "PLUMBING";

    protected override void VerifySafety()
    {
        Console.WriteLine($"║ SAFETY CHECK");
        Console.WriteLine($"║   ✓ Water main isolated");
        Console.WriteLine($"║   ✓ Pressure relief valve tested");
        Console.WriteLine($"║   ✓ Work area drained");
    }

    protected override void PerformInspection()
    {
        Console.WriteLine($"║ INSPECTION");
        Console.WriteLine($"║   → Pressure test at 3 bar for 15 minutes");
        Console.WriteLine($"║   → Joint integrity check on all connections");
        Console.WriteLine($"║   → Flow rate measurement at outlet");
    }

    protected override void RecordFindings()
    {
        Console.WriteLine($"║ FINDINGS");
        AddFinding("Pressure held at 3 bar — no drop detected");
        AddFinding("All joints sealed correctly");
        AddFinding("Flow rate 12 L/min — within specification");
    }

    protected override void AttachComplianceNote()
    {
        Console.WriteLine($"║ COMPLIANCE");
        Console.WriteLine($"║   SANS 10400-W water installation standard — COMPLIANT");
    }
}

// ── Concrete Class — Electrical ────────────────────────────

public class ElectricalInspection : JobInspectionReport
{
    protected override string TradeName => "ELECTRICAL";

    protected override void VerifySafety()
    {
        Console.WriteLine($"║ SAFETY CHECK");
        Console.WriteLine($"║   ✓ Circuit isolated at DB board");
        Console.WriteLine($"║   ✓ Lockout tag applied");
        Console.WriteLine($"║   ✓ Tested for live wires — confirmed dead");
    }

    protected override void PerformInspection()
    {
        Console.WriteLine($"║ INSPECTION");
        Console.WriteLine($"║   → Insulation resistance test");
        Console.WriteLine($"║   → Earth continuity verification");
        Console.WriteLine($"║   → RCD trip time measurement");
    }

    protected override void RecordFindings()
    {
        Console.WriteLine($"║ FINDINGS");
        AddFinding("Insulation resistance 250 MΩ — well above 1 MΩ minimum");
        AddFinding("Earth continuity 0.3 Ω — acceptable");
        AddFinding("RCD tripped at 24 ms — within 40 ms requirement");
    }

    // Override the hook — electrical work requires photo evidence
    protected override void CaptureAdditionalPhotos()
    {
        Console.WriteLine($"║ PHOTO EVIDENCE");
        Console.WriteLine($"║   📷 DB board before and after");
        Console.WriteLine($"║   📷 Certification label close-up");
    }

    protected override void AttachComplianceNote()
    {
        Console.WriteLine($"║ COMPLIANCE");
        Console.WriteLine($"║   Certificate of Compliance issued — Registered Electrician #RE-4471");
    }
}

// ── Concrete Class — Carpentry ─────────────────────────────

public class CarpentryInspection : JobInspectionReport
{
    protected override string TradeName => "CARPENTRY";

    protected override void VerifySafety()
    {
        Console.WriteLine($"║ SAFETY CHECK");
        Console.WriteLine($"║   ✓ Cable scan completed before cutting");
        Console.WriteLine($"║   ✓ Dust extraction in place");
        Console.WriteLine($"║   ✓ PPE worn throughout");
    }

    protected override void PerformInspection()
    {
        Console.WriteLine($"║ INSPECTION");
        Console.WriteLine($"║   → Level and plumb verification");
        Console.WriteLine($"║   → Fixing point load test");
        Console.WriteLine($"║   → Finish and edge quality review");
    }

    protected override void RecordFindings()
    {
        Console.WriteLine($"║ FINDINGS");
        AddFinding("All surfaces level within 2 mm tolerance");
        AddFinding("Fixings rated for 45 kg — exceeds 25 kg requirement");
        AddFinding("Edges sanded and sealed");
    }

    protected override void AttachComplianceNote()
    {
        Console.WriteLine($"║ COMPLIANCE");
        Console.WriteLine($"║   Non-structural work — no building plan approval required");
    }
}

// ── Client Code ────────────────────────────────────────────

class Program
{
    static void Main()
    {
        var inspections = new JobInspectionReport[]
        {
            new PlumbingInspection(),
            new ElectricalInspection(),
            new CarpentryInspection(),
        };

        var jobs = new[]
        {
            ("JC-601", "James Nghipandua"),
            ("JC-602", "Anna Nghoshi"),
            ("JC-603", "Peter Nangolo"),
        };

        for (int i = 0; i < inspections.Length; i++)
            inspections[i].GenerateReport(jobs[i].Item1, jobs[i].Item2);
    }
}

Output (abbreviated):

╔══════════════════════════════════════════════════
║ INSPECTION REPORT — PLUMBING
╠══════════════════════════════════════════════════
║ Job ID    : #JC-601
║ Inspector : James Nghipandua
║ Opened    : 01 Sep 2025 08:14
╠══════════════════════════════════════════════════
║ SAFETY CHECK
║   ✓ Water main isolated
║   ✓ Pressure relief valve tested
║   ✓ Work area drained
║ INSPECTION
║   → Pressure test at 3 bar for 15 minutes
║   → Joint integrity check on all connections
║   → Flow rate measurement at outlet
║ FINDINGS
║   • Pressure held at 3 bar — no drop detected
║   • All joints sealed correctly
║   • Flow rate 12 L/min — within specification
║ COMPLIANCE
║   SANS 10400-W water installation standard — COMPLIANT
╠══════════════════════════════════════════════════
║ Findings recorded : 3
║ Signed off by     : James Nghipandua
║ Closed            : 01 Sep 2025 08:14
╚══════════════════════════════════════════════════

╔══════════════════════════════════════════════════
║ INSPECTION REPORT — ELECTRICAL
╠══════════════════════════════════════════════════
║ Job ID    : #JC-602
║ Inspector : Anna Nghoshi
...
║ FINDINGS
║   • Insulation resistance 250 MΩ — well above 1 MΩ minimum
║   • Earth continuity 0.3 Ω — acceptable
║   • RCD tripped at 24 ms — within 40 ms requirement
║ PHOTO EVIDENCE                          ← hook overridden
║   📷 DB board before and after
║   📷 Certification label close-up
║ COMPLIANCE
║   Certificate of Compliance issued — Registered Electrician #RE-4471
...

Note that only ElectricalInspection produces a PHOTO EVIDENCE section — it's the only subclass that overrode the hook. Plumbing and carpentry inherit the empty default and skip that step entirely.


TypeScript Implementation

// ── Report/JobInspectionReport.ts ──────────────────────────

export abstract class JobInspectionReport {
  protected jobId:     string = "";
  protected inspector: string = "";
  protected findings:  string[] = [];

  // ══════════════════════════════════════════════════════
  // THE TEMPLATE METHOD
  // TypeScript has no 'sealed' — convention is to document
  // that subclasses must not override this method.
  // ══════════════════════════════════════════════════════
  generateReport(jobId: string, inspector: string): void {
    this.jobId     = jobId;
    this.inspector = inspector;
    this.findings  = [];

    this.openReport();                // concrete
    this.verifySafety();              // abstract
    this.performInspection();         // abstract
    this.recordFindings();            // abstract
    this.captureAdditionalPhotos();   // hook
    this.attachComplianceNote();      // abstract
    this.signOff();                   // concrete
  }

  // ── Concrete steps ─────────────────────────────────────

  private openReport(): void {
    const now = new Date().toLocaleString("en-GB", {
      day: "2-digit", month: "short", year: "numeric",
      hour: "2-digit", minute: "2-digit",
    });

    console.log(`\n╔══════════════════════════════════════════════════`);
    console.log(`║ INSPECTION REPORT — ${this.tradeName}`);
    console.log(`╠══════════════════════════════════════════════════`);
    console.log(`║ Job ID    : #${this.jobId}`);
    console.log(`║ Inspector : ${this.inspector}`);
    console.log(`║ Opened    : ${now}`);
    console.log(`╠══════════════════════════════════════════════════`);
  }

  private signOff(): void {
    const now = new Date().toLocaleString("en-GB", {
      day: "2-digit", month: "short", year: "numeric",
      hour: "2-digit", minute: "2-digit",
    });

    console.log(`╠══════════════════════════════════════════════════`);
    console.log(`║ Findings recorded : ${this.findings.length}`);
    console.log(`║ Signed off by     : ${this.inspector}`);
    console.log(`║ Closed            : ${now}`);
    console.log(`╚══════════════════════════════════════════════════`);
  }

  // ── Abstract steps ─────────────────────────────────────

  protected abstract get tradeName(): string;
  protected abstract verifySafety():         void;
  protected abstract performInspection():    void;
  protected abstract recordFindings():       void;
  protected abstract attachComplianceNote(): void;

  // ── Hook — empty default ───────────────────────────────

  protected captureAdditionalPhotos(): void {
    // Default: no additional photos required
  }

  // ── Helper ─────────────────────────────────────────────

  protected addFinding(finding: string): void {
    this.findings.push(finding);
    console.log(`║   • ${finding}`);
  }
}

// ── Report/PlumbingInspection.ts ───────────────────────────

import { JobInspectionReport } from "./JobInspectionReport";

export class PlumbingInspection extends JobInspectionReport {
  protected get tradeName(): string { return "PLUMBING"; }

  protected verifySafety(): void {
    console.log(`║ SAFETY CHECK`);
    console.log(`║   ✓ Water main isolated`);
    console.log(`║   ✓ Pressure relief valve tested`);
    console.log(`║   ✓ Work area drained`);
  }

  protected performInspection(): void {
    console.log(`║ INSPECTION`);
    console.log(`║   → Pressure test at 3 bar for 15 minutes`);
    console.log(`║   → Joint integrity check on all connections`);
    console.log(`║   → Flow rate measurement at outlet`);
  }

  protected recordFindings(): void {
    console.log(`║ FINDINGS`);
    this.addFinding("Pressure held at 3 bar — no drop detected");
    this.addFinding("All joints sealed correctly");
    this.addFinding("Flow rate 12 L/min — within specification");
  }

  protected attachComplianceNote(): void {
    console.log(`║ COMPLIANCE`);
    console.log(`║   SANS 10400-W water installation standard — COMPLIANT`);
  }
}

// ── Report/ElectricalInspection.ts ─────────────────────────

import { JobInspectionReport } from "./JobInspectionReport";

export class ElectricalInspection extends JobInspectionReport {
  protected get tradeName(): string { return "ELECTRICAL"; }

  protected verifySafety(): void {
    console.log(`║ SAFETY CHECK`);
    console.log(`║   ✓ Circuit isolated at DB board`);
    console.log(`║   ✓ Lockout tag applied`);
    console.log(`║   ✓ Tested for live wires — confirmed dead`);
  }

  protected performInspection(): void {
    console.log(`║ INSPECTION`);
    console.log(`║   → Insulation resistance test`);
    console.log(`║   → Earth continuity verification`);
    console.log(`║   → RCD trip time measurement`);
  }

  protected recordFindings(): void {
    console.log(`║ FINDINGS`);
    this.addFinding("Insulation resistance 250 MΩ — well above 1 MΩ minimum");
    this.addFinding("Earth continuity 0.3 Ω — acceptable");
    this.addFinding("RCD tripped at 24 ms — within 40 ms requirement");
  }

  // Override the hook — electrical work requires photo evidence
  protected override captureAdditionalPhotos(): void {
    console.log(`║ PHOTO EVIDENCE`);
    console.log(`║   📷 DB board before and after`);
    console.log(`║   📷 Certification label close-up`);
  }

  protected attachComplianceNote(): void {
    console.log(`║ COMPLIANCE`);
    console.log(`║   Certificate of Compliance issued — Registered Electrician #RE-4471`);
  }
}

// ── Report/CarpentryInspection.ts ──────────────────────────

import { JobInspectionReport } from "./JobInspectionReport";

export class CarpentryInspection extends JobInspectionReport {
  protected get tradeName(): string { return "CARPENTRY"; }

  protected verifySafety(): void {
    console.log(`║ SAFETY CHECK`);
    console.log(`║   ✓ Cable scan completed before cutting`);
    console.log(`║   ✓ Dust extraction in place`);
    console.log(`║   ✓ PPE worn throughout`);
  }

  protected performInspection(): void {
    console.log(`║ INSPECTION`);
    console.log(`║   → Level and plumb verification`);
    console.log(`║   → Fixing point load test`);
    console.log(`║   → Finish and edge quality review`);
  }

  protected recordFindings(): void {
    console.log(`║ FINDINGS`);
    this.addFinding("All surfaces level within 2 mm tolerance");
    this.addFinding("Fixings rated for 45 kg — exceeds 25 kg requirement");
    this.addFinding("Edges sanded and sealed");
  }

  protected attachComplianceNote(): void {
    console.log(`║ COMPLIANCE`);
    console.log(`║   Non-structural work — no building plan approval required`);
  }
}

// ── App.ts ─────────────────────────────────────────────────

import { JobInspectionReport  } from "./Report/JobInspectionReport";
import { PlumbingInspection   } from "./Report/PlumbingInspection";
import { ElectricalInspection } from "./Report/ElectricalInspection";
import { CarpentryInspection  } from "./Report/CarpentryInspection";

const inspections: JobInspectionReport[] = [
  new PlumbingInspection(),
  new ElectricalInspection(),
  new CarpentryInspection(),
];

const jobs: [string, string][] = [
  ["JC-601", "James Nghipandua"],
  ["JC-602", "Anna Nghoshi"],
  ["JC-603", "Peter Nangolo"],
];

inspections.forEach((inspection, i) => {
  const [jobId, inspector] = jobs[i];
  inspection.generateReport(jobId, inspector);
});

C# vs TypeScript — Key Differences

Aspect C# TypeScript
Locking the template Not virtual — cannot be overridden No sealed equivalent — document by convention
Abstract property protected abstract string TradeName { get; } protected abstract get tradeName(): string
Hook default protected virtual void Method() { } protected method(): void { } — concrete, overridable
Override keyword override required override optional (recommended, TS 4.3+)
Access modifier protected enforced at compile time protected enforced at compile time (erased at runtime)

The inability to seal a method in TypeScript is the notable gap. In C#, a non-virtual method simply cannot be overridden, the compiler enforces the algorithm structure. In TypeScript, a subclass can always override any method, so the template method's integrity depends on convention and code review rather than the type system.


Template Method vs Strategy

Both let you vary behaviour. The difference is inheritance vs composition:

Template Method Strategy
Mechanism Inheritance — subclass overrides steps Composition — object holds a strategy
What varies Individual steps within a fixed algorithm The entire algorithm
When chosen Compile time — subclass is fixed Runtime — strategy can be swapped
Structure control Base class controls the sequence Strategy controls everything
FixIt Pro Inspection sequence fixed, steps vary per trade Pricing algorithm entirely swappable

Template Method is the inheritance-based answer to varying behaviour. Strategy is the composition-based answer. Where both fit, prefer Strategy, composition is more flexible and testable. Template Method earns its place when the sequence itself is the thing you need to protect.


The Hollywood Principle

The Template Method is the clearest expression of the Hollywood Principle:

"Don't call us, we'll call you."

Subclasses don't call the base class to run the algorithm. The base class calls the subclasses' steps at the right moments. Control flow is inverted, this is inversion of control at the class level, and it's the same principle that underpins dependency injection and framework design generally.


When to Use the Template Method

Use it when:

  • Several classes share the same algorithm structure but differ in specific steps
  • You want to control which parts of an algorithm subclasses can change
  • You have duplicated code across subclasses that follows a common sequence
  • You want to provide optional extension points via hooks

Avoid it when:

  • The algorithm structure itself needs to vary then use Strategy instead
  • The inheritance hierarchy becomes deep or the base class accumulates too many abstract steps
  • Composition would give you the same result with more flexibility

Real-World Takeaway

The Template Method is the backbone of framework design. In ASP.NET Core, ControllerBase and the MVC request pipeline call your action methods at fixed points in a sequence you don't control. In xUnit and NUnit, the test lifecycle, setup, test, teardown, is a template method; you fill in the steps. In Angular, lifecycle hooks (ngOnInit, ngOnChanges, ngOnDestroy) are template method steps called by the framework in a guaranteed order. React class components work the same way with componentDidMount and friends. Stream.CopyTo() in .NET is a template method calling Read and Write, implemented differently by every stream type.

In FixIt Pro, the inspection sequence is defined once. Adding a fourth trade, lets say, HvacInspection, means implementing four abstract methods and optionally one hook. The sequence, the report header, the sign-off block, and the findings counter all come free. And if regulations change and a new step is needed between findings and compliance, it's added in one place and every trade picks it up automatically.


Repo Structure for This Episode

github.com/antonlungameni/fixit-pro-design-patterns

fixit-pro-design-patterns/
├── csharp/Behavioral/21-TemplateMethod/
│   ├── Report/JobInspectionReport.cs
│   ├── Report/PlumbingInspection.cs
│   ├── Report/ElectricalInspection.cs
│   ├── Report/CarpentryInspection.cs
│   └── Program.cs
└── typescript/Behavioral/21-TemplateMethod/
    ├── Report/JobInspectionReport.ts
    ├── Report/PlumbingInspection.ts
    ├── Report/ElectricalInspection.ts
    ├── Report/CarpentryInspection.ts
    └── App.ts

Previous: #20 — Strategy Pattern
Next up: #22 — Visitor Pattern — the final episode Running operations across a job card structure — audit, PDF export, cost rollup, without modifying a single job card class.