Define the skeleton of an algorithm in a base class, and let subclasses fill in the details β without changing the overall structure. Like a recipe that says "follow these steps in this order, but make each step your own way."
Template Method in one line: "Here's the recipe β follow these steps in this order, but make each step your own way." The base class is the head chef who decides the cooking process. Subclasses are the line cooks who prepare each dish differently, but they all follow the same sequence.
What: Imagine you're following a recipe for making a hot drink. The steps are always the same β boil water, brew the drink, pour it into a cup, add extras. But what you brew (tea leaves vs coffee beans) and what extras you add (lemon vs milk) change depending on the drink. That's the Template Method patternA behavioral design pattern where a base class defines the overall structure of an algorithm (the "template"), locking the step order in place, while letting subclasses override specific steps. Think of it as a recipe: the order is fixed, but each cook can customize individual steps.. A base class defines the overall recipe β the skeleton of an algorithmThe fixed sequence of steps that makes up the algorithm. In Template Method, this skeleton is defined in a single method (the "template method") that calls other methods in a specific order. Subclasses can change what each step does, but they can never change the order of steps. β and locks it in place. Subclasses fill in the specific steps. The key word is "skeleton" β the order of steps is fixed, only the details change.
The base class is an abstract classA class that can't be instantiated directly β you must create a subclass that fills in the missing pieces. In Template Method, the abstract class contains the template method (the recipe) and declares abstract methods for the steps that subclasses must implement. It's the blueprint that says "here's the process, now you fill in the blanks." that contains the template method and declares the steps subclasses must fill in. Each subclass is a concrete classA non-abstract class that provides real implementations for all the abstract methods defined by the base class. If the base says "Brew()" is abstract, the Tea concrete class implements Brew() as "steep tea leaves" and the Coffee concrete class implements it as "run water through grounds." β it provides the actual implementation for each customizable step. Some steps are optional β these are called hook methodsA method in the base class that has a default implementation (usually empty or returning a default value). Subclasses CAN override it if they want, but they don't have to. Hooks are the "optional customization points" β like the recipe saying "add condiments if you like." Abstract methods are mandatory; hooks are optional. β the base class provides a default (usually empty) implementation, and subclasses can override them if they want to, but they don't have to.
When: Use Template Method when multiple classes share the same workflow but differ in specific steps. Classic examples: data processing pipelines (read β transform β write), document generators (header β body β footer), game loops (input β update β render), test frameworks (setup β test β teardown). If the flow is always the same but the details change β that's your signal.
In C# / .NET: .NET uses Template Method heavily. BackgroundService.ExecuteAsync()In ASP.NET Core, BackgroundService is an abstract class for long-running background tasks. The framework handles the lifecycle β StartAsync(), StopAsync(), disposal. You just override ExecuteAsync() to say what your service does. That's Template Method: the framework owns the skeleton, you fill in one step. β the framework handles start/stop lifecycle, you just override ExecuteAsync to say what your service does. TagHelper.Process()In ASP.NET Core Razor, TagHelper is a base class for creating custom HTML tag processors. The framework calls Init() and then Process() at the right time during page rendering. You override Process() to transform the tag's output β Template Method in action. in Razor β you override Process() to generate HTML, the framework calls it at the right time. Controller action filters β OnActionExecuting/OnActionExecuted follow a template. Even xUnit β constructor is setup, Dispose is cleanup, the framework runs them in order around your test.
Quick Code:
TemplateMethod-at-a-glance.cs
// A data export pipeline β the steps are always the same,
// but HOW each step works depends on the format.
public abstract class DataExporter
{
// The template method β sealed so nobody can mess with the order
public sealed void Export()
{
ReadData();
TransformData();
WriteOutput();
OnExportComplete(); // hook β optional
}
protected abstract void ReadData(); // subclass MUST implement
protected abstract void TransformData(); // subclass MUST implement
protected abstract void WriteOutput(); // subclass MUST implement
protected virtual void OnExportComplete() { } // hook β CAN override
}
public class CsvExporter : DataExporter
{
protected override void ReadData() => Console.WriteLine("Reading from database...");
protected override void TransformData() => Console.WriteLine("Flattening to CSV rows...");
protected override void WriteOutput() => Console.WriteLine("Writing .csv file...");
}
public class JsonExporter : DataExporter
{
protected override void ReadData() => Console.WriteLine("Reading from API...");
protected override void TransformData() => Console.WriteLine("Serializing to JSON...");
protected override void WriteOutput() => Console.WriteLine("Writing .json file...");
protected override void OnExportComplete()
=> Console.WriteLine("Sending Slack notification...");
}
// Usage β the caller doesn't care which exporter it is
DataExporter exporter = new JsonExporter();
exporter.Export(); // always: Read β Transform β Write β Hook
Section 2
Prerequisites
Template Method is built on top of a few fundamental OOP concepts. If any of these feel unfamiliar, take a few minutes to review them first β it'll make everything on this page click much faster.
Before diving in:
Abstract Classes & Methods β An abstract classA class marked with the 'abstract' keyword in C#. You can't create an instance of it directly β you must create a subclass. Abstract classes can contain both implemented methods (with code) and abstract methods (just the signature, no code). Subclasses are forced to implement the abstract methods. is like a form with some fields already filled in and some left blank for you to complete. You can't submit the blank form itself β someone has to fill in the blanks first. In Template Method, the abstract class IS the template.
Method Overriding β When a parent class defines a virtual methodA method marked with the 'virtual' keyword in C#. It means "I have a default implementation, but subclasses are allowed to replace it with their own version using the 'override' keyword." If a method is 'abstract', subclasses MUST override it. If it's 'virtual', overriding is optional., child classes can replace that behavior with their own version using the override keyword. And the sealed keyword on an override prevents further overriding β this is how Template Method locks down the algorithm skeleton.
Inheritance Basics β How a child class extends a parent and inherits its behavior. Template Method is one of the few patterns where inheritance is the right choice (most other patterns favor composition).
Access Modifiers β Especially protected, which means "visible to subclasses but invisible to the outside world." Template Method uses protected abstract for the steps that subclasses must fill in β the outside world only sees the public template method.
Interfaces vs Abstract Classes β Knowing the difference helps you understand why Template Method uses abstract classes instead of interfaces. Interfaces define "what" (the contract), but abstract classes can define "what" AND provide shared "how" (the skeleton). That shared skeleton is the whole point.
Section 3
Real-World Analogies
Making Hot Beverages
When you make tea or coffee, you follow the same basic recipe: boil water, steep or brew, pour into a cup, add your extras. The recipe β the template β is identical every time. What changes is the details of each step.
The recipe book is the abstract base class. The four steps written in order is the template method. Each barista who follows the recipe their own way is a concrete subclass. And "Add condiments" is a hook methodAn optional step in the template. The recipe says "add condiments if you like" β some drinks skip this step entirely (black coffee), while others customize it (lemon for tea, milk for latte). In code, a hook has a default empty implementation that subclasses can optionally override. β some drinks skip it entirely (black coffee), while others customize it heavily (lemon and honey for tea).
Step order (boil β brew β pour β condiments)
The locked-down sequence that nobody can change
sealed template method
Brewing step (varies by drink)
A step that each subclass MUST implement differently
abstract method
Adding condiments (optional)
A step that subclasses CAN customize but don't have to
virtual hook method
Barista following the recipe their way
A specific implementation of the template steps
Concrete subclass
Reading the diagram: The rectangles are fixed steps that every drink shares. The diamonds are decision points where subclasses plug in their own behavior β "Brew" is abstract (required), "Add Condiments" is a virtual hook (optional, shown with a dashed border).
Tax Filing Form
The government gives you a form with fixed sections: income, deductions, credits, signature. The structure never changes β you just fill in YOUR numbers. The IRS decides the order and the rules; you just provide the data. That's a template method: fixed structure, custom content.
Assembly Line
A car factory has the same stations in the same order: frame β engine β paint β interior. Every car goes through the same line, but a sedan and an SUV get different parts at each station. The line manager (base class) decides the order. The workers at each station (subclasses) do the specific work.
School Exam
Every exam has the same format: read instructions, answer section A, answer section B, submit. The template is fixed β your answers are the custom part. The school board designs the structure, students fill in the content. And the order is enforced: you can't submit before you start.
Section 4
Core Pattern & UML
The Gang of FourThe four authors (Gamma, Helm, Johnson, Vlissides) who wrote "Design Patterns: Elements of Reusable Object-Oriented Software" in 1994. Their 23 patterns β including Template Method β became the foundation of software design vocabulary. "GoF" is the standard shorthand. definition says: "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."
Let's break that down in plain English. "Skeleton of an algorithm" means the fixed sequence of steps. Think of a skeleton in biology β it gives the body structure, but the muscles, skin, and organs (the details) can vary from person to person. In code, the skeleton is a method that calls other methods in a specific order. "Deferring some steps" means the base class says "I know WHEN this step should happen, but I don't know HOW to do it β that's your job, subclass." And "without changing the structure" means subclasses can customize what each step does, but they can never reorder, skip, or add steps. The skeleton is locked.
This is the Hollywood Principle"Don't call us, we'll call you." In Template Method, the base class controls the flow and calls subclass methods at the right time. The subclass never decides when to run β it just waits to be called. Like an actor at an audition: the director (base class) calls "Action!" and the actor (subclass) performs. in action: "Don't call us, we'll call you." The base class is in charge. It calls the subclass methods at the right time β the subclass never calls the template method itself.
UML Class Diagram
If you're new to UML: the dashed arrows with hollow arrowheads mean "inherits from." Both ConcreteClassA and ConcreteClassB extend AbstractClass β they inherit the sealed TemplateMethod() and provide their own implementations of the abstract primitive operations. The pseudocode note on the right shows what TemplateMethod() does internally: it calls the steps in a fixed order.
Notice three types of methods in the AbstractClass:
The template method itself (sealed) β the skeleton. No subclass can change it. It controls WHEN each step runs.
Primitive operations (abstract) β the mandatory customization points. Every subclass MUST implement these. They control WHAT happens at each step.
Hooks (virtual with a default) β the optional customization points. Subclasses CAN override these, but they work fine with the default behavior too.
Participant
Role
Responsibility
AbstractClassThe base class that defines the template method and declares abstract/virtual steps. It owns the algorithm's structure. In C#, this is an abstract class with a sealed method for the skeleton and protected abstract/virtual methods for the steps.
Template Owner
Defines the template method (the skeleton) and declares abstract/virtual steps. Owns the algorithm structure β subclasses can't change it.
ConcreteClassA subclass that provides real implementations for all abstract methods and optionally overrides hooks. Each concrete class represents one "flavor" of the algorithm β same skeleton, different details.
Step Implementor
Implements every abstract method and optionally overrides hooks. Provides the specific behavior for each step while respecting the skeleton's order.
Template MethodThe single method in the base class that defines the algorithm's skeleton. It calls primitive operations and hooks in a fixed order. Marked 'sealed' in C# to prevent subclasses from overriding it. This is the "recipe" β the thing that enforces the step order.
Algorithm Skeleton
The sealed method that calls steps in a fixed order. This IS the pattern β it enforces the sequence and delegates each step to subclass implementations.
Primitive OperationsAbstract methods declared in the base class that subclasses MUST implement. These are the mandatory customization points β the blanks that every concrete class must fill in. They represent the steps that vary between implementations.
Required Steps
Abstract methods that every subclass must implement. These are the steps that MUST vary β the base class knows WHEN to run them but not HOW.
Hook MethodsVirtual methods in the base class with a default implementation (often empty). Subclasses CAN override them to inject optional behavior, but they're not forced to. Hooks are the "nice to have" customization points β like condiments on a drink.
Optional Steps
Virtual methods with a default (usually empty). Subclasses override them only when they need optional behavior β like logging, notifications, or conditional logic.
Why sealed Matters
In C#, marking the template method as sealed override (or just defining it as non-virtual in the base) ensures that no subclass can override the skeleton. This is the pattern's safety net β it guarantees that the algorithm structure stays intact, no matter how many subclasses you create. Without sealed, a subclass could accidentally (or intentionally) override the template method and break the entire flow. The sealed keyword turns a "please don't override this" convention into a compile-time guarantee.
Section 5
Code Implementations
Now that you understand the concept and UML, let's see Template Method in real C# code. We'll look at three approaches: the classic abstract class (textbook GoF), a modern async version with records, and a functional approach that skips inheritance entirely.
Recommended
Start here. This is the textbook Template Method β easiest to understand and the most common in codebases. Once you get this version, the async and functional variants will feel natural.
We'll build a data export pipeline. Every export follows the same flow: read data, transform it, write the output. But how each step works depends on the format β CSV reads from a database and flattens rows, JSON reads from an API and serializes objects. The template method locks the order; subclasses fill in the details.
DataExporter.cs
/// <summary>
/// Base class that defines the export algorithm skeleton.
/// Subclasses implement the individual steps β but the ORDER is locked.
/// </summary>
public abstract class DataExporter
{
// The template method β sealed so no subclass can change the flow
public sealed void Export()
{
Console.WriteLine($"[{GetType().Name}] Starting export...");
ReadData(); // Step 1: mandatory β subclass decides HOW
TransformData(); // Step 2: mandatory β subclass decides HOW
WriteOutput(); // Step 3: mandatory β subclass decides HOW
OnExportComplete(); // Hook: optional β subclass CAN customize
Console.WriteLine($"[{GetType().Name}] Export finished.");
}
// --- Primitive operations (abstract = MUST implement) ---
protected abstract void ReadData();
protected abstract void TransformData();
protected abstract void WriteOutput();
// --- Hook method (virtual = CAN override, default does nothing) ---
protected virtual void OnExportComplete() { }
}
CsvExporter.cs
/// <summary>CSV exporter β reads from DB, flattens to rows, writes .csv</summary>
public class CsvExporter : DataExporter
{
protected override void ReadData()
=> Console.WriteLine(" Reading rows from SQL database...");
protected override void TransformData()
=> Console.WriteLine(" Flattening nested objects to flat CSV rows...");
protected override void WriteOutput()
=> Console.WriteLine(" Writing to output.csv...");
// Doesn't override OnExportComplete β default (nothing) is fine
}
JsonExporter.cs
/// <summary>JSON exporter β reads from API, serializes, writes .json</summary>
public class JsonExporter : DataExporter
{
protected override void ReadData()
=> Console.WriteLine(" Fetching data from REST API...");
protected override void TransformData()
=> Console.WriteLine(" Serializing to JSON with camelCase...");
protected override void WriteOutput()
=> Console.WriteLine(" Writing to output.json...");
// Overrides the hook to add post-export notification
protected override void OnExportComplete()
=> Console.WriteLine(" Sending Slack notification: export done!");
}
Program.cs
DataExporter exporter = new JsonExporter();
exporter.Export();
// Output:
// [JsonExporter] Starting export...
// Fetching data from REST API...
// Serializing to JSON with camelCase...
// Writing to output.json...
// Sending Slack notification: export done!
// [JsonExporter] Export finished.
Walking through the code: The Export() method is sealed β nobody can override it. It calls four methods in order. Three are abstract (ReadData, TransformData, WriteOutput) β every subclass MUST provide these. One is a virtual hook (OnExportComplete) β subclasses CAN override it but don't have to. CsvExporter skips the hook. JsonExporter uses it to send a Slack message. Both follow the exact same flow. Click through each tab to see how the base class defines the skeleton, and then each subclass fills in just the parts that differ β without touching the overall algorithm.
In real-world .NET, most I/O operations are async β database calls, HTTP requests, file writes. Here's how Template Method adapts to the async/awaitC#'s mechanism for writing non-blocking code. An 'async' method returns a Task and can 'await' other async operations without blocking the thread. This is critical for scalability β your server can handle other requests while waiting for a database query or API call to complete. world, with records for immutable results and CancellationTokenA .NET mechanism that allows cooperative cancellation of long-running operations. When a user cancels a request or a timeout fires, the token signals "stop what you're doing." Well-designed async code checks this token periodically and bails out gracefully instead of running to completion. for graceful shutdown.
AsyncDataPipeline.cs
// Immutable result record β no setters, clear contract
public record PipelineResult(bool Success, int RecordsProcessed, string? Error = null);
/// <summary>
/// Async template method with cancellation support.
/// Each step returns a Task β the skeleton awaits them in order.
/// </summary>
public abstract class AsyncDataPipeline<T>
{
public sealed async Task<PipelineResult> ProcessAsync(CancellationToken ct = default)
{
try
{
var data = await ValidateAsync(ct); // Step 1
var transformed = await TransformAsync(data, ct); // Step 2
var count = await LoadAsync(transformed, ct); // Step 3
await OnCompleteAsync(count, ct); // Hook
return new PipelineResult(true, count);
}
catch (OperationCanceledException)
{
return new PipelineResult(false, 0, "Cancelled");
}
catch (Exception ex)
{
return new PipelineResult(false, 0, ex.Message);
}
}
protected abstract Task<IReadOnlyList<T>> ValidateAsync(CancellationToken ct);
protected abstract Task<IReadOnlyList<T>> TransformAsync(IReadOnlyList<T> data, CancellationToken ct);
protected abstract Task<int> LoadAsync(IReadOnlyList<T> data, CancellationToken ct);
// Hook β default does nothing, subclasses can add logging/metrics
protected virtual Task OnCompleteAsync(int count, CancellationToken ct)
=> Task.CompletedTask;
}
// Concrete: imports user data from an external API
public class ApiDataPipeline : AsyncDataPipeline<UserDto>
{
private readonly HttpClient _http;
public ApiDataPipeline(HttpClient http) => _http = http;
protected override async Task<IReadOnlyList<UserDto>> ValidateAsync(CancellationToken ct)
{
var users = await _http.GetFromJsonAsync<List<UserDto>>("/api/users", ct);
return users?.Where(u => u.IsActive).ToList() ?? [];
}
protected override Task<IReadOnlyList<UserDto>> TransformAsync(
IReadOnlyList<UserDto> data, CancellationToken ct)
{
// Normalize emails to lowercase
var result = data.Select(u => u with { Email = u.Email.ToLowerInvariant() }).ToList();
return Task.FromResult<IReadOnlyList<UserDto>>(result);
}
protected override async Task<int> LoadAsync(
IReadOnlyList<UserDto> data, CancellationToken ct)
{
// Bulk insert to database
await _db.BulkInsertAsync(data, ct);
return data.Count;
}
protected override async Task OnCompleteAsync(int count, CancellationToken ct)
=> Console.WriteLine($"Imported {count} users from API");
}
public record UserDto(string Name, string Email, bool IsActive);
What changed from the classic version? Three things: (1) Every step is async Task instead of void β the skeleton awaits each one. (2) CancellationToken flows through every step so the pipeline can bail out gracefully. (3) A PipelineResult record captures success/failure immutably β no mutable state floating around. The pattern structure is identical β just adapted for modern async .NET.
What if you want the template structure without inheritance? Sometimes creating a whole class hierarchy for a three-step pipeline feels like overkill. Instead, you can pass the steps as delegate functionsIn C#, a delegate (like Func<T> or Action) is a reference to a method. You can pass methods around as variables β store them, pass them to other methods, or invoke them later. This lets you swap behavior at runtime without subclassing. β same skeleton, zero inheritance.
FuncPipeline.cs
/// <summary>
/// Template Method without inheritance β pass steps as Func delegates.
/// The skeleton is still fixed; the steps are still pluggable.
/// </summary>
public static class DataPipeline
{
public static void Process(
Func<string> read, // Step 1: returns raw data
Func<string, string> transform, // Step 2: transforms data
Action<string> write, // Step 3: writes output
Action? onComplete = null) // Hook: optional callback
{
Console.WriteLine("Pipeline started...");
var raw = read();
var transformed = transform(raw);
write(transformed);
onComplete?.Invoke();
Console.WriteLine("Pipeline finished.");
}
}
// Usage β define steps inline with lambdas
DataPipeline.Process(
read: () => File.ReadAllText("input.csv"),
transform: raw => raw.ToUpperInvariant(),
write: data => File.WriteAllText("output.csv", data),
onComplete: () => Console.WriteLine("CSV export done!")
);
// Or reuse named methods
DataPipeline.Process(
read: DatabaseReader.FetchAll,
transform: JsonTransformer.Normalize,
write: BlobStorage.Upload
);
When to use this approach: When you have a simple, one-off pipeline and creating a class hierarchy feels heavy. The trade-off is clear: you lose the ability to group related state and behavior into a class, but you gain simplicity and flexibility. For complex pipelines with shared state, stick with the abstract class. For lightweight scripts and utilities, the functional approach is cleaner.
Trade-off
The functional approach works great for simple cases, but it doesn't scale well when steps need to share state, when you need multiple related overrides, or when you want DI integration. For production services, the abstract class approach is almost always better.
Which Approach Should You Use?
Approach
Best For
Pros
Cons
Classic Abstract Class
Learning, most production codebases, DI-friendly services
Clear structure, easy to understand, enforces step contract at compile time
Requires inheritance (one base class only in C#)
Modern Async
I/O-bound pipelines, ASP.NET Core services, anything using HttpClient or EF Core
Slightly more boilerplate (Task, CancellationToken on every step)
Func-Based
One-off scripts, utilities, quick prototypes
No class hierarchy, ultra-lightweight, great for simple 2-3 step flows
No shared state between steps, doesn't scale for complex pipelines
For most ASP.NET Core applications, start with the async abstract class β it covers cancellation and error handling out of the box. For learning or simple synchronous flows, the classic version is perfect. The func-based approach is a quick tool for one-off tasks where creating a class hierarchy feels like overkill.
Section 6
Jr vs Sr Implementation
This section shows how the same problem gets solved very differently by a junior and a senior developer. The junior copy-pastes the entire workflow for each report format, while the senior extracts the shared skeleton into a base class. The difference is dramatic β and it's the whole reason Template Method exists.
Problem Statement
Build a report generator that supports PDF, Excel, and CSV output. Every report follows the same flow: gather data from a database, format headers, format rows, and write the file. The data source and output path are configurable. New formats (HTML, Markdown) should be easy to add later without touching existing code.
How a Junior Thinks
"I'll just make three methods β GeneratePdf(), GenerateExcel(), GenerateCsv(). Easy!"
ReportService.cs β Junior Version
public class ReportService
{
public void GeneratePdf(string dataSource, string outputPath)
{
// Step 1: Gather data (SAME for every format)
var data = Database.Query(dataSource);
Console.WriteLine($"Loaded {data.Count} records");
// Step 2: Format headers (differs by format)
var header = string.Join(" | ", data.First().Keys);
var pdfHeader = $"
{header}
";
// Step 3: Format rows (differs by format)
var rows = data.Select(r => $"
{string.Join(", ", r.Values)}
");
// Step 4: Write file (differs by format)
File.WriteAllText(outputPath, pdfHeader + string.Join("", rows));
Console.WriteLine("PDF report saved.");
}
public void GenerateExcel(string dataSource, string outputPath)
{
// Step 1: Gather data (COPY-PASTED from above)
var data = Database.Query(dataSource);
Console.WriteLine($"Loaded {data.Count} records");
// Step 2: Format headers (Excel-specific)
var header = string.Join("\t", data.First().Keys);
// Step 3: Format rows (Excel-specific)
var rows = data.Select(r => string.Join("\t", r.Values));
// Step 4: Write file (same structure, different extension)
File.WriteAllText(outputPath, header + "\n" + string.Join("\n", rows));
Console.WriteLine("Excel report saved.");
}
public void GenerateCsv(string dataSource, string outputPath)
{
// Step 1: Gather data (COPY-PASTED again!)
var data = Database.Query(dataSource);
Console.WriteLine($"Loaded {data.Count} records");
// Step 2: Format headers (CSV-specific)
var header = string.Join(",", data.First().Keys);
// Step 3: Format rows (CSV-specific)
var rows = data.Select(r => string.Join(",", r.Values));
// Step 4: Write file
File.WriteAllText(outputPath, header + "\n" + string.Join("\n", rows));
Console.WriteLine("CSV report saved.");
}
}
Problems
90% Code Duplication
Step 1 (data gathering) and the overall flow are identical across all three methods. Every time you fix a bug in data loading, you have to find and fix it in three places. Miss one? Congratulations β you now have inconsistent behavior.
Rigid Format Logic
Need to add a new format like HTML or Markdown? Copy-paste an entire method, change a few lines, and pray you didn't introduce a typo. Every new format means another 20+ lines of mostly-duplicated code.
Untestable
You can't test data loading separately from formatting. You can't test formatting separately from file writing. Everything is tangled into one giant method per format β to test one step, you have to run the entire pipeline.
Ready to see the fix?
Click here (or the "Senior Approach" tab above) to see how a senior developer eliminates all three problems with Template Method.
How a Senior Thinks
"The flow is identical β gather, format headers, format rows, write. Only the formatting differs. That's a template method waiting to happen."
ReportGenerator.cs
/// <summary>
/// The template: sealed Generate() locks the flow.
/// Subclasses fill in FormatHeader and FormatRows.
/// </summary>
public abstract class ReportGenerator
{
public sealed void Generate(string dataSource, string outputPath)
{
var data = GatherData(dataSource); // shared β same for all
var header = FormatHeader(data); // abstract β varies by format
var rows = FormatRows(data); // abstract β varies by format
WriteOutput(outputPath, header, rows); // shared β same for all
OnReportComplete(outputPath); // hook β optional
}
// Shared steps β implemented here, not overridable
private List<Dictionary<string, object>> GatherData(string source)
{
var data = Database.Query(source);
Console.WriteLine($"Loaded {data.Count} records");
return data;
}
private void WriteOutput(string path, string header, string body)
{
File.WriteAllText(path, header + "\n" + body);
Console.WriteLine($"Report written to {path}");
}
// Abstract steps β each format implements differently
protected abstract string FormatHeader(List<Dictionary<string, object>> data);
protected abstract string FormatRows(List<Dictionary<string, object>> data);
// Hook β subclasses can optionally add post-processing
protected virtual void OnReportComplete(string outputPath) { }
}
public class ExcelReport : ReportGenerator
{
protected override string FormatHeader(List<Dictionary<string, object>> data)
{
return string.Join("\t", data.First().Keys);
}
protected override string FormatRows(List<Dictionary<string, object>> data)
{
return string.Join("\n",
data.Select(r => string.Join("\t", r.Values)));
}
// No hook override β default (nothing) is fine for Excel
}
Program.cs
// DI registration
builder.Services.AddTransient<ReportGenerator, PdfReport>();
// Swap to: AddTransient<ReportGenerator, ExcelReport>();
// Usage β caller doesn't know which format
var generator = app.Services.GetRequiredService<ReportGenerator>();
generator.Generate("SELECT * FROM Sales", "output.pdf");
// Or resolve by name with a factory:
ReportGenerator GetReport(string format) => format switch
{
"pdf" => new PdfReport(),
"excel" => new ExcelReport(),
"csv" => new CsvReport(),
_ => throw new ArgumentException($"Unknown format: {format}")
};
Design Decisions
Sealed Template Method
Generate() is sealed, so no subclass can break the gather β format β write flow. The order is guaranteed at compile time β not by convention or documentation, but by the language itself.
Hook for Extensibility
OnReportComplete() is a virtual hook with an empty default. PdfReport uses it to log a download-ready message. ExcelReport skips it entirely. Future formats can use it for email notifications, metrics, or cleanup β without changing the base class.
DI-Friendly
Each concrete report class can be registered in the DI container and injected wherever needed. Want to swap PDF for Excel? Change one line in Program.cs. Want to test formatting logic? Inject the concrete class directly and call FormatHeader() through a test subclass. No mocking needed.
The Bottom Line
Here's the math that makes the case for Template Method:
Formats
Junior (copy-paste per format)
Senior (Template Method)
3 (PDF, Excel, CSV)
3 methods with ~90% duplicated code
1 base class + 3 subclasses (zero duplication)
5 (+ HTML, Markdown)
5 methods β fix a bug in data loading? Edit 5 places
2 new subclasses β base class fix applies everywhere
10 formats
10 methods β 900+ lines of mostly-identical code
10 small subclasses β shared logic written once
The senior approach grows linearly with zero duplication. The junior approach multiplies every line of shared logic by the number of formats. This is why Template Method exists β it trades copy-paste for a single source of truth.
Section 7
Evolution of Template Method in .NET
Template Method has been part of .NET since day one, but the way developers write it has evolved dramatically over two decades. Let's walk through the major milestones β from the early days of object-everywhere to modern async-first sealed templates.
Era
Key Feature
Template Method Impact
.NET 1.0 (2002)
Abstract classes
Manual abstract classes with object returns β functional but no type safety
.NET 2.0 (2005)
Generics
Type-safe templates β compiler catches mismatches, no runtime casts
.NET 3.5 (2007)
LINQ & lambdas
Functional twist β algorithms accept Func<T> for customizable steps instead of inheritance
.NET Core (2016+)
Framework extensibility
BackgroundService, TagHelper, HealthCheck β Template Method becomes the standard .NET API design
.NET 8+ (2023+)
Async + sealed + primary ctors
Async-first templates with sealed methods, CancellationToken, and compile-time safety
.NET 1.0 β Manual Abstract Classes
In the early days of .NET (2002), there were no generics, no async, no LINQ. Everything was typed as object, which meant lots of casting and runtime errors. Template Method existed, but it was clunky.
.NET 1.0 Style
// Everything is 'object' β no type safety
public abstract class DataProcessor
{
public void Process()
{
object data = Load(); // returns object β could be anything
object result = Transform(data);
Save(result);
}
protected abstract object Load();
protected abstract object Transform(object data);
protected abstract void Save(object data);
}
public class CsvProcessor : DataProcessor
{
protected override object Load()
{
// Must cast everywhere β runtime errors if wrong
return File.ReadAllLines("data.csv");
}
protected override object Transform(object data)
{
var lines = (string[])data; // dangerous cast!
return lines.Select(l => l.ToUpper()).ToArray();
}
protected override void Save(object data)
{
var lines = (string[])data; // another dangerous cast!
File.WriteAllLines("output.csv", lines);
}
}
Limitations
No type safety β every step returns object and you cast at runtime. A typo in the cast causes a InvalidCastException at runtime, not a compile error. No async support. No way to express "this processor handles CSV strings specifically."
.NET 2.0 β Generics Revolution
.NET 2.0 (2005) introduced genericsA language feature that lets you write classes and methods with type parameters. Instead of using 'object' and casting, you declare DataProcessor<TInput, TOutput> and the compiler enforces type safety. No runtime casts, no InvalidCastException β errors are caught at compile time., and Template Method got a massive upgrade. Now you could express "this processor takes strings and outputs integers" at the type level β the compiler catches mismatches before the code even runs.
.NET 2.0 Style
// Generic template β type-safe, no casting needed
public abstract class DataProcessor<TInput, TOutput>
{
public TOutput Process()
{
TInput data = Load(); // strongly typed
TOutput result = Transform(data); // compiler checks types
Save(result);
return result;
}
protected abstract TInput Load();
protected abstract TOutput Transform(TInput data);
protected abstract void Save(TOutput data);
}
// Concrete: CSV strings in, report object out
public class CsvProcessor : DataProcessor<string[], ReportData>
{
protected override string[] Load()
=> File.ReadAllLines("data.csv"); // returns string[] β type-safe!
protected override ReportData Transform(string[] data)
{
// No casting β 'data' is already string[]
var rows = data.Select(line => line.Split(',')).ToList();
return new ReportData(rows);
}
protected override void Save(ReportData data)
=> data.WriteToDisk("output.report");
}
The Big Win
Generics eliminated casting bugs entirely. If you define DataProcessor<string[], ReportData>, the compiler guarantees that Load() returns string[] and Transform() returns ReportData. Wrong types? Compile error, not runtime crash.
.NET 3.5 (2007) β LINQ and IEnumerable as Template Method
.NET 3.5 introduced LINQLanguage Integrated Query β a set of extension methods (Where, Select, OrderBy, GroupBy, etc.) that let you query collections, databases, and XML using a consistent syntax. LINQ brought functional programming concepts into mainstream C# and changed how developers think about data transformation. and lambda expressions, and with them came a subtle but important shift in how developers thought about Template Method. You might not realize it, but every time you call OrderBy(), you're using a form of the pattern.
Think about it this way: the sorting algorithm is fixed β the framework handles partitioning, comparing, and swapping elements. But what to sort by? That's your customizable step. Instead of overriding an abstract method in a subclass, you pass a Func<T, TKey> β a tiny function that says "use this property for comparison." The template is the algorithm; your lambda is the custom step.
The same idea applies to IEnumerable<T> with yield returnA C# keyword that turns a method into an iterator. Instead of building a whole collection and returning it, yield return produces one element at a time β lazily, on demand. The framework controls the iteration protocol (MoveNext/Current), and your code just says what data comes next.. The iteration protocol is fixed β the framework calls MoveNext() to advance and reads Current to get the value. You customize what data appears by writing a method with yield return. The framework owns the "how to iterate" template; you fill in "what to iterate over."
LINQ OrderBy β Template Method via Func<T>
var products = new List<Product>
{
new("Laptop", 999.99m, 4.5),
new("Mouse", 29.99m, 4.8),
new("Monitor", 349.99m, 4.2)
};
// The SORTING ALGORITHM is fixed (the template).
// Your lambda is the CUSTOM STEP β what to sort by.
// Sort by price β you provide the "key selector" step
var byPrice = products.OrderBy(p => p.Price);
// Sort by rating descending β same algorithm, different step
var byRating = products.OrderByDescending(p => p.Rating);
// Chain multiple steps β the framework handles the multi-key sort
var byPriceThenRating = products
.OrderBy(p => p.Price)
.ThenByDescending(p => p.Rating);
// Where() is similar β the FILTERING algorithm is fixed,
// you provide the PREDICATE step (what passes the filter)
var affordable = products.Where(p => p.Price < 500m);
IEnumerable + yield return β Template Method for Iteration
// The iteration PROTOCOL is fixed: MoveNext() β Current β MoveNext() β ...
// You customize WHAT DATA appears using yield return.
public static IEnumerable<int> Fibonacci()
{
int a = 0, b = 1;
while (true) // framework calls MoveNext() to advance
{
yield return a; // framework reads Current to get this value
(a, b) = (b, a + b);
}
}
// The framework controls iteration β you just defined what comes next
foreach (var n in Fibonacci().Take(10))
Console.Write($"{n} ");
// Output: 0 1 1 2 3 5 8 13 21 34
// Custom data pipeline β each step is a template with your logic plugged in
public static IEnumerable<string> ProcessLogs(IEnumerable<string> lines)
{
foreach (var line in lines)
{
if (string.IsNullOrWhiteSpace(line)) continue; // your filter step
yield return $"[{DateTime.UtcNow:HH:mm:ss}] {line.Trim()}"; // your transform step
}
}
The Seed of Functional Template Method
LINQ planted an important idea: you don't always need inheritance to implement Template Method. If the "customizable step" is a single function, you can accept a Func<T> or Action<T> parameter instead of requiring a subclass. The algorithm stays fixed, but the variable step comes from a lambda β not from overriding an abstract method. This functional approach became increasingly popular in later .NET versions, and it's the style shown in Section 5's "functional variant."
.NET Core β Framework-Level Template Methods
With .NET Core (2016+), Microsoft started using Template Method as the standard pattern for framework extensibility. The most visible example is BackgroundServiceAn abstract class in Microsoft.Extensions.Hosting that provides the template for long-running background tasks. The framework handles StartAsync, StopAsync, and disposal. You override just one method β ExecuteAsync β to define what your service does. It's Template Method built into the framework itself. β the framework owns the lifecycle (start, stop, dispose), and you just override ExecuteAsync() to say what your service does.
.NET Core Style
// BackgroundService IS a template method:
// Framework owns: StartAsync() β ExecuteAsync() β StopAsync()
// You own: just ExecuteAsync()
public class OrderCleanupService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<OrderCleanupService> _logger;
public OrderCleanupService(
IServiceScopeFactory scopeFactory,
ILogger<OrderCleanupService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
// Your one customization point β the rest is framework-managed
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var stale = await db.Orders
.Where(o => o.Status == "Pending" && o.CreatedAt < DateTime.UtcNow.AddDays(-30))
.ToListAsync(stoppingToken);
db.Orders.RemoveRange(stale);
await db.SaveChangesAsync(stoppingToken);
_logger.LogInformation("Cleaned {Count} stale orders", stale.Count);
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
}
// Registration β one line
builder.Services.AddHostedService<OrderCleanupService>();
Microsoft Embraced the Pattern
BackgroundService is just the most famous example. TagHelper, ControllerBase, MinimalApiEndpoint, HealthCheck β they all follow the same template: the framework owns the lifecycle and calls your overrides at the right time. If you're writing .NET, you're already using Template Method whether you know it or not.
Modern .NET 8+ β Async + Sealed + Primary Constructors
Modern C# (2023+) brings primary constructors, file-scoped namespaces, and a strong convention of async-first APIs. Combined with sealed template methods and CancellationToken everywhere, this is the best version of the pattern yet.
Modern .NET 8+ Style
// Modern template: async, sealed, primary constructor for DI
public abstract class NotificationPipeline(
ILogger logger,
IMetrics metrics)
{
public sealed async Task<bool> SendAsync(
Notification notification,
CancellationToken ct = default)
{
using var activity = metrics.StartTimer("notification.send");
logger.LogInformation("Sending {Type} notification", GetType().Name);
if (!await ValidateAsync(notification, ct))
{
logger.LogWarning("Validation failed for {Id}", notification.Id);
return false;
}
var formatted = await FormatAsync(notification, ct);
await DeliverAsync(formatted, ct);
await OnSentAsync(notification, ct); // hook
logger.LogInformation("Sent {Type} in {Elapsed}ms",
GetType().Name, activity.ElapsedMilliseconds);
return true;
}
protected abstract Task<bool> ValidateAsync(Notification n, CancellationToken ct);
protected abstract Task<string> FormatAsync(Notification n, CancellationToken ct);
protected abstract Task DeliverAsync(string content, CancellationToken ct);
// Hook β default: nothing. Override to add metrics, audit logs, etc.
protected virtual Task OnSentAsync(Notification n, CancellationToken ct)
=> Task.CompletedTask;
}
// Concrete: Email notifications
public class EmailPipeline(
ILogger logger, IMetrics metrics,
ISmtpClient smtp)
: NotificationPipeline(logger, metrics)
{
protected override Task<bool> ValidateAsync(Notification n, CancellationToken ct)
=> Task.FromResult(!string.IsNullOrEmpty(n.Recipient));
protected override Task<string> FormatAsync(Notification n, CancellationToken ct)
=> Task.FromResult($"<html><body><h1>{n.Subject}</h1><p>{n.Body}</p></body></html>");
protected override async Task DeliverAsync(string content, CancellationToken ct)
=> await smtp.SendAsync(content, ct);
protected override async Task OnSentAsync(Notification n, CancellationToken ct)
=> await metrics.IncrementAsync("email.sent", ct);
}
Best Practice Today
Async-first (all steps return Task), sealed template method (compile-time safety), primary constructors for clean DI, CancellationToken on every async method, and structured logging throughout. This is the modern gold standard for Template Method in .NET.
Section 8
Template Method in the .NET Framework
Template Method isn't just something you build β it's baked into the .NET framework itself. Every time you create a background service, write a custom tag helper, or implement an action filter, you're filling in the blanks of a template that Microsoft designed. Here are five places where you've probably used Template Method without realizing it.
Each of these follows the same idea: the framework owns the process (the template), and you fill in the specific step that matters for your use case. Let's look at each one in detail.
BackgroundService
The most common Template Method in modern .NET. The framework handles the entire service lifecycle β StartAsync() sets things up, StopAsync() triggers graceful shutdown, and Dispose() cleans up resources. You override just one method: ExecuteAsync(). That's your custom step β the one piece of the template that you own.
PriceUpdateService.cs
public class PriceUpdateService(
ILogger<PriceUpdateService> logger,
IPriceFeed feed) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Price feed starting...");
await foreach (var price in feed.StreamAsync(stoppingToken))
{
logger.LogDebug("Updated {Symbol}: {Price}", price.Symbol, price.Value);
// The framework calls StartAsync β ExecuteAsync β StopAsync
// You only define what happens INSIDE ExecuteAsync
}
}
}
TagHelper
Razor tag helpers let you create custom HTML elements. The framework calls Init() first (for setup), then Process() or ProcessAsync() (your custom step). You override Process() to transform the tag's HTML output. The framework handles discovery, parsing, and rendering β you just say what the tag should output.
EmailTagHelper.cs
// Usage in Razor: <email address="user@example.com" />
// Renders: <a href="mailto:user@example.com">user@example.com</a>
[HtmlTargetElement("email")]
public class EmailTagHelper : TagHelper
{
[HtmlAttributeName("address")]
public string Address { get; set; } = "";
// Your custom step β the framework calls this at render time
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
output.Attributes.SetAttribute("href", $"mailto:{Address}");
output.Content.SetContent(Address);
}
}
Controller Action Filters
ASP.NET Core's action filter pipeline follows a clear template: OnActionExecuting() runs before your controller action, the action runs, then OnActionExecuted() runs after. The framework controls this order β you just override the "before" and "after" hooks to inject cross-cutting behavior like validation or logging.
AuditFilter.cs
public class AuditFilter : ActionFilterAttribute
{
// Before the action runs
public override void OnActionExecuting(ActionExecutingContext context)
{
var user = context.HttpContext.User.Identity?.Name ?? "anonymous";
var action = context.ActionDescriptor.DisplayName;
Console.WriteLine($"[AUDIT] {user} calling {action}");
}
// After the action runs
public override void OnActionExecuted(ActionExecutedContext context)
{
var status = context.Exception != null ? "FAILED" : "OK";
Console.WriteLine($"[AUDIT] Action completed: {status}");
}
}
// Usage: decorate any controller or action
[AuditFilter]
public class OrdersController : ControllerBase { ... }
Stream
The abstract Stream class defines the template for all I/O in .NET. It provides shared logic for position tracking, length calculations, and async wrappers β but the actual reading and writing of bytes is left to subclasses. FileStream reads from disk, MemoryStream reads from RAM, NetworkStream reads from a socket. Same template, different implementations.
CountingStream.cs
/// <summary>
/// Custom stream that counts bytes read β wraps any other stream.
/// You override Read/Write/etc β the framework calls them.
/// </summary>
public class CountingStream : Stream
{
private readonly Stream _inner;
public long BytesRead { get; private set; }
public CountingStream(Stream inner) => _inner = inner;
public override int Read(byte[] buffer, int offset, int count)
{
int bytesRead = _inner.Read(buffer, offset, count);
BytesRead += bytesRead;
return bytesRead;
}
public override void Write(byte[] buffer, int offset, int count)
=> _inner.Write(buffer, offset, count);
// Required overrides for the Stream template
public override bool CanRead => _inner.CanRead;
public override bool CanSeek => _inner.CanSeek;
public override bool CanWrite => _inner.CanWrite;
public override long Length => _inner.Length;
public override long Position
{
get => _inner.Position;
set => _inner.Position = value;
}
public override void Flush() => _inner.Flush();
public override long Seek(long offset, SeekOrigin origin)
=> _inner.Seek(offset, origin);
public override void SetLength(long value)
=> _inner.SetLength(value);
}
HttpMessageHandler
DelegatingHandlerAn abstract class in System.Net.Http that acts as a building block for HTTP message handler chains. Each handler can inspect or modify HTTP requests/responses, then pass them to the next handler in the chain. You override SendAsync() to add behavior before and/or after the inner handler runs. is how you intercept HTTP requests in .NET's HttpClient pipeline. The framework chains handlers together β each one gets a chance to inspect or modify the request/response before passing it along. You override SendAsync() to add your behavior.
LoggingHandler.cs
public class LoggingHandler(ILogger<LoggingHandler> logger)
: DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken ct)
{
// Before β log the request
logger.LogInformation("β {Method} {Uri}", request.Method, request.RequestUri);
var sw = Stopwatch.StartNew();
// Call the next handler in the chain (the template's "inner" step)
var response = await base.SendAsync(request, ct);
// After β log the response
sw.Stop();
logger.LogInformation("β {Status} in {Elapsed}ms",
(int)response.StatusCode, sw.ElapsedMilliseconds);
return response;
}
}
// Registration β add to HttpClient pipeline
builder.Services.AddHttpClient("api")
.AddHttpMessageHandler<LoggingHandler>();
Section 9
When To Use / When Not To
Use When
Multiple classes share the same overall workflow but differ in specific steps — for example, different report generators that all follow "gather data → format → write" but format differently
You need to enforce a specific step ordering — the algorithm must always run steps A, B, C in that exact sequence, and subclasses should never be able to rearrange them
You want to eliminate copy-paste code across similar classes — five export classes all have identical setup and cleanup logic, only the "export" step differs
You're building a framework or library where users customize specific steps — think BackgroundService in .NET: the framework handles lifecycle, you just override ExecuteAsync
Your workflow follows a setup → process → cleanup pattern — the setup and cleanup are always the same, but the processing step changes depending on the context
Don't Use When
There's only one implementation and you don't expect more — creating an abstract base class for a single subclass is over-engineering
The steps vary wildly between classes — if every subclass overrides every step and the shared skeleton is basically empty, you don't have a real template
You need to swap algorithms at runtime — Template Method locks the algorithm at compile time via inheritance. If you need runtime flexibility, use StrategyStrategy lets you define a family of interchangeable algorithms and swap them at runtime through composition. Unlike Template Method (which locks the algorithm structure via inheritance), Strategy injects the entire algorithm as an object, making it easy to change behavior on the fly. instead
The "template" has just one step — if there's only one customizable method and no surrounding skeleton logic, you're really just doing plain polymorphismPolymorphism means "many forms." In OOP, it's the ability to call the same method on different objects and get different behavior. A method override in a subclass is the simplest form of polymorphism — you don't need a full Template Method pattern for that.
All steps are identical across implementations — if nothing actually varies, there's no need for an abstract class with overridable methods. Just use a concrete class
Decision Framework
Section 10
Comparisons
Template Method looks similar to a few other patterns because they all deal with "customizable behavior." Here's how to tell them apart so you always reach for the right tool.
Template Method vs Strategy
This is the most commonly confused pair. Both define algorithms with varying parts — but they do it in completely different ways.
Template Method
Uses inheritance — subclass overrides specific steps in a base class
The skeleton is fixed in the base class — subclasses can't change the step order
Customization happens at compile time (you pick a subclass)
The base class calls the subclass — this is the Hollywood Principle"Don't call us, we'll call you." The base class controls the flow and calls subclass methods when needed, rather than the subclass calling the base. This inverts control — the framework drives execution, your code just fills in the blanks.: "don't call us, we'll call you"
Best for: fixed workflows with a few varying steps
VS
Strategy
Uses composition — context holds a strategy object and delegates to it
The entire algorithm is pluggable — the strategy object defines all steps
Customization happens at runtime (swap the strategy object anytime)
The context delegates to the strategy — no inheritance required
Best for: interchangeable algorithms where you need runtime flexibility
Template Method vs Factory Method
These two are closely related — in fact, Factory Method is a special case of Template Method.
Template Method
Defines a complete algorithm skeleton with multiple customizable steps
Steps can be anything: validation, formatting, sending, logging, etc.
The template method orchestrates the full workflow
Multiple abstract/hook methods can be overridden
Example: DataExporter with steps: connect, query, format, write, disconnect
VS
Factory MethodFactory Method defines a method in a base class for creating objects, but lets subclasses decide which concrete class to instantiate. It's essentially Template Method where the customizable "step" is object creation rather than a workflow step.
Defines a single customizable step: object creation
The step being overridden specifically creates and returns an object
The base class uses the created object in its workflow
Usually just one abstract method (CreateProduct())
Example: DocumentApp with one override: CreateDocument() returns Word/PDF/HTML
Template Method vs State
Both involve objects that behave differently depending on context — but the mechanism and timing are very different.
Template Method
Behavior is fixed at compile time — you pick a subclass and that's it
The algorithm structure never changes — only specific steps differ
Uses inheritance: subclass IS-A base class
One object, one behavior, forever
Example: PdfReport always formats as PDF
VS
StateState pattern lets an object change its behavior when its internal state changes. The object holds a reference to a "state" object, and delegates behavior to it. When the state changes, the object swaps the state object, making it appear to change its class at runtime.
Behavior changes at runtime — the same object acts differently over time
The entire behavior can shift when the state transitions
Uses composition: context HAS-A state object
One object, many behaviors, depending on current state
Example: Order behaves differently when Pending vs Shipped vs Delivered
Quick rule: Template Method = fixed skeleton, varying steps, inheritance, compile-time. Strategy = pluggable algorithm, composition, runtime. Factory Method = Template Method where the step is object creation. State = same object, different behavior based on current state.
Section 11
SOLID Mapping
Template Method has an interesting relationship with SOLID — it strongly supports some principles but requires care with others. Here's how each principle connects.
Principle
Relation
Explanation
SRPSingle Responsibility Principle — a class should have only one reason to change. Each class handles one job, one concern, one slice of functionality.
Supports
Each concrete subclass handles exactly one variant of the algorithm. PdfReportGenerator knows how to format PDFs, CsvReportGenerator knows CSVs. The base class handles the shared workflow — everyone has a single, clear responsibility.
OCPOpen/Closed Principle — classes should be open for extension but closed for modification. You add new behavior by creating new classes, not by editing existing ones.
Supports
Need a new report format? Create a new subclass and override the formatting step. The base class and all existing subclasses remain untouched. This is exactly what OCP asks for — extend without modifying.
LSPLiskov Substitution Principle — subclasses must be usable wherever the base class is expected, without breaking the program. If code works with the base type, it must also work with any subtype.
Depends
If subclasses honor the base class contract (return expected types, don't throw unexpected exceptions, don't skip required steps), LSP is maintained. But if a subclass overrides a hook in a way that breaks the algorithm's assumptions — like returning null where the next step expects a value — LSP is violated.
ISPInterface Segregation Principle — clients should not be forced to depend on methods they don't use. Prefer small, focused interfaces over large, bloated ones.
Depends
If the base class forces too many abstract methods, subclasses must implement steps they don't need. A base with 12 abstract methods where most subclasses only care about 3 is an ISP violation. Fix: use hooks (virtual methods with default no-op implementations) for optional steps, and only make truly required steps abstract.
DIPDependency Inversion Principle — high-level modules should depend on abstractions, not concrete implementations. Both high-level and low-level modules should depend on interfaces or abstract classes.
Caution
Template Method uses inheritance from a concrete-ish base class (even if abstract, it contains real logic). High-level code depends on the abstraction, which is good. But subclasses are tightly coupled to the base class — any change to the base ripples down. This is weaker than depending on a pure interface, which is what DIP ideally wants.
Section 12
Bug Case Studies
Six real-world bugs that crop up when Template Method is implemented incorrectly. Each one was discovered in production β some silently, some explosively. Study the patterns so you catch them before your users do.
Bug 1: Forgetting to Call the Base Method
The Incident
Notification system. The team has a NotificationSender base class with a template method that runs: validate recipient → format message → send → log result. The FormatMessage() step is a virtual hook that adds a default header and footer around the message body.
A developer creates SmsNotificationSender and overrides FormatMessage() to truncate messages to 160 characters. They write the truncation logic but forget to call base.FormatMessage(). The SMS messages go out without the standard header/footer, breaking compliance requirements. Nobody notices for two weeks because SMS messages "looked fine" in quick manual testing — the missing header was only caught during a compliance audit.
What Went Wrong
Think of it like a recipe: the base class says "always add salt and pepper at step 2." A subclass comes along and replaces step 2 with "add paprika" but forgets to include the salt and pepper. The dish still looks like food, it still gets served — but it's missing essential seasoning that the original recipe guaranteed. In code terms, the override replaced the base behavior instead of extending it. The subclass wrote its own truncation logic but never called base.FormatMessage() to include the compliance header/footer that the base method provides.
Time to Diagnose
2 weeks — the messages were functional (they delivered), so no errors appeared in logs. The missing header was only spotted during a scheduled compliance review. The fix took 30 seconds; finding the bug took 14 days.
BuggySmsSender.cs
public class NotificationSender
{
// Template method β the skeleton
public void Send(Notification notification)
{
Validate(notification);
var formatted = FormatMessage(notification); // step 2
Deliver(formatted);
LogResult(notification);
}
protected virtual string FormatMessage(Notification n)
{
// Default: adds compliance header + footer
return $"[OFFICIAL] {n.Body}\n-- Sent by SystemCorp";
}
// ... other methods
}
public class SmsNotificationSender : NotificationSender
{
// β Overrides FormatMessage but forgets base.FormatMessage()
protected override string FormatMessage(Notification n)
{
// Truncates to 160 chars... but never adds the header/footer!
return n.Body.Length > 160 ? n.Body[..160] : n.Body;
}
}
What went wrong: The developer replaced the entire formatting logic instead of extending it. The base method adds the compliance header and footer, but the override skips it entirely. The truncation works, but the legally-required header is gone.
FixedSmsSender.cs
public class SmsNotificationSender : NotificationSender
{
protected override string FormatMessage(Notification n)
{
// β Call base first to get header + footer
var formatted = base.FormatMessage(n);
// Then truncate the result
return formatted.Length > 160 ? formatted[..160] : formatted;
}
}
The fix: Call base.FormatMessage() first to get the standard formatting, then apply the SMS-specific truncation on top. Now the compliance header is always included.
Lesson Learned
If a hook method has default behavior that must always run, consider making it non-virtual and providing a separate extension point. Or document clearly: "Always call base.FormatMessage() when overriding." Better yet, separate the required logic from the customizable part so developers can't accidentally skip it.
How to Spot This in Your Code
Search for every override of a virtual method that has a non-empty base implementation. If the override body doesn't contain base.MethodName(), ask: "Was the base behavior intentionally discarded, or accidentally forgotten?" Code review tip: any override of a method with default logic should have a comment explaining why base is or isn't called. You can also add a unit test that checks the output includes the expected header/footer — if a new subclass skips it, the test catches it immediately.
Bug 2: Template Method Not Sealed
The Incident
E-commerce checkout. The CheckoutProcessor base class has a template method ProcessCheckout() that runs: validate cart → calculate totals → charge payment → send confirmation. A junior developer creating ExpressCheckoutProcessor accidentally overrides ProcessCheckout() itself (not a step method) to "simplify" the express flow.
The result? Express checkout skips payment validation entirely. Orders go through with invalid credit cards. The team discovers it when the payment gateway rejects a batch of charges 3 days later — $47,000 in orders that were "confirmed" but never actually paid for.
What Went Wrong
Imagine a factory assembly line where the manager posts the exact order of operations on the wall: inspect parts, assemble, test, package. Now imagine a new employee rips the poster off the wall and writes their own order — they skip the testing step entirely. That's what happened here. The template method is the poster — it defines the mandatory sequence. When a subclass overrides the template method itself (instead of individual steps), the entire guaranteed sequence is gone. The subclass wrote its own "simplified" flow that accidentally dropped the payment step.
Time to Diagnose
3 days — confirmation emails went out (the override included that step), so customers believed they'd paid. The payment gateway's batch rejection report was the first signal. Tracing back to the overridden template method took another 2 hours of git blame.
BuggyCheckout.cs
public abstract class CheckoutProcessor
{
// β Not sealed β subclasses can override the ENTIRE skeleton!
public virtual void ProcessCheckout(Cart cart)
{
ValidateCart(cart);
var totals = CalculateTotals(cart);
ChargePayment(cart, totals); // critical step!
SendConfirmation(cart, totals);
}
protected abstract void ValidateCart(Cart cart);
// ...
}
public class ExpressCheckoutProcessor : CheckoutProcessor
{
// β Overrides the template method itself β replaces the skeleton!
public override void ProcessCheckout(Cart cart)
{
// "Simplified" β but skips ChargePayment entirely!
ValidateCart(cart);
var totals = CalculateTotals(cart);
SendConfirmation(cart, totals); // β no payment!
}
}
FixedCheckout.cs
public abstract class CheckoutProcessor
{
// β Not virtual β no subclass can override the skeleton
public void ProcessCheckout(Cart cart)
{
ValidateCart(cart);
var totals = CalculateTotals(cart);
ChargePayment(cart, totals);
SendConfirmation(cart, totals);
}
// β Only these steps are customizable
protected abstract void ValidateCart(Cart cart);
protected virtual decimal CalculateTotals(Cart cart) => cart.Sum();
protected abstract void ChargePayment(Cart cart, decimal totals);
protected virtual void SendConfirmation(Cart cart, decimal totals)
{
// default confirmation email
}
}
The fix: Remove virtual from the template method. In C#, a non-virtual method can't be overridden (the compiler will reject it). This guarantees the skeleton — validate, calculate, charge, confirm — always runs in order, and subclasses can only customize individual steps.
Lesson Learned
The template method should never be virtual. Make it a regular (non-virtual) public method. This is the entire point of the pattern — the skeleton is locked, only the steps are customizable. In languages like Java, use final. In C#, simply don't mark it virtual.
How to Spot This in Your Code
Grep your codebase for public virtual on any method that acts as a template (calls multiple steps in sequence). If the template method is virtual or override-able, it's a ticking time bomb. In C#, your template method should be a plain public void (or public async Task) — no virtual keyword. In code reviews, flag any subclass that overrides a method containing more than one step call — that's almost certainly overriding the skeleton, not a step.
Bug 3: Constructor Calling Virtual Methods
The Incident
Application startup. A DataImporter base class calls Initialize() in its constructor to set up the import pipeline. Initialize() is a virtual method that subclasses override. The problem? When the base constructor runs, the derived class hasn't been constructed yet — its fields are still null/default.
The SqlDataImporter subclass overrides Initialize() to set up a database connection using a connection string stored in a field. But that field is assigned in the derived constructor, which hasn't run yet when the base constructor calls Initialize(). Result: NullReferenceException on every startup attempt.
What Went Wrong
Think of it like building a house: the foundation (base constructor) is poured first, and then the walls (derived constructor) go up. Now imagine the foundation crew tries to hang a picture on a wall that hasn't been built yet — there's nothing there to hold the nail. That's exactly what happens when a base constructor calls a virtual method. The base constructor runs first, calls the overridden method in the subclass, but the subclass's own fields haven't been assigned yet because its constructor hasn't run. The method tries to use a field that's still null, and the whole thing crashes.
Time to Diagnose
45 minutes — the stack trace pointed to the constructor, but the developer initially blamed the connection string configuration. Once they realized the base constructor was calling a virtual method before the derived class was ready, the fix was straightforward.
BuggyConstructor.cs
public abstract class DataImporter
{
protected DataImporter()
{
Initialize(); // β Calls virtual method in constructor!
}
protected virtual void Initialize()
{
// Base setup
}
public void Import() { /* template method */ }
}
public class SqlDataImporter : DataImporter
{
private readonly string _connectionString;
public SqlDataImporter(string connStr)
: base() // base constructor runs FIRST
{
_connectionString = connStr; // this runs SECOND
}
protected override void Initialize()
{
// β _connectionString is still null here!
var conn = new SqlConnection(_connectionString); // NullReferenceException
conn.Open();
}
}
What happens step by step: (1) new SqlDataImporter("Server=...") is called. (2) The base constructor runs first — it calls Initialize(). (3) Because Initialize() is virtual, the derived override runs. (4) But _connectionString hasn't been assigned yet (that happens in step 5). (5) The derived constructor finally assigns _connectionString — too late.
FixedConstructor.cs
public abstract class DataImporter
{
// β No virtual calls in constructor
protected DataImporter() { }
// β Initialize is called as part of the template, not the constructor
public void Import()
{
Initialize(); // safe β object is fully constructed
FetchData();
TransformData();
SaveData();
Cleanup();
}
protected virtual void Initialize() { }
protected abstract void FetchData();
protected abstract void TransformData();
protected abstract void SaveData();
protected virtual void Cleanup() { }
}
public class SqlDataImporter : DataImporter
{
private readonly string _connectionString;
public SqlDataImporter(string connStr) : base()
{
_connectionString = connStr; // β assigned before Initialize() runs
}
protected override void Initialize()
{
// β _connectionString is now properly set
var conn = new SqlConnection(_connectionString);
conn.Open();
}
}
Lesson Learned
Never call virtual or abstract methods from a constructor. The C# compiler even has a warning for this (CA2214). Move initialization logic into the template method itself — make "initialize" the first step of the algorithm, not something that happens during object creation.
How to Spot This in Your Code
Enable the CA2214 code analysis rule in your project — it flags virtual calls inside constructors at compile time. You can also search for constructors that call any method marked virtual, abstract, or override. If you see a NullReferenceException thrown from a constructor and the null field is clearly assigned in the derived class, that's the classic symptom of this bug.
Bug 4: Async Void in Template Steps
The Incident
Data import pipeline. A DataPipeline base class defines steps: extract, transform, load. A developer creates ApiDataPipeline where the extract step calls an external HTTP API. They mark the override as async void instead of returning Task. The pipeline "completes successfully" in milliseconds — but the actual HTTP call fires off into the background and its result is never captured.
The import dashboard shows "Success" for every run. But the database is empty — the extracted data never arrives because the async voidIn C#, async void methods are "fire-and-forget" — the caller cannot await them, cannot catch their exceptions, and has no idea when they finish. Async Task methods, on the other hand, return a Task that the caller can await, giving proper control flow and error handling. async void should only be used for event handlers. method detaches from the pipeline's execution flow. Worse: when the HTTP call fails, the exception crashes the entire application process because async void exceptions are unobservable.
What Went Wrong
Imagine you tell a courier "deliver this package and come back when you're done." But instead of waiting, you walk away immediately and mark it as "delivered" in your notebook. The courier is still out there, running around — but your records say the job is finished. That's async void in a nutshell. When a method returns void, the caller has nothing to wait on. The await inside the method pauses that method, but the caller has already moved on. The template method called Extract(), got back instantly (because async void returns to the caller at the first await), and proceeded to Transform() and Load() on empty data.
Time to Diagnose
4 hours — the "success" status was deeply misleading. The developer checked the API (working), checked the transform logic (fine), checked the database writes (fine). Only when they added timing logs did they notice the extract step was completing in 0ms for an HTTP call that should take 200ms.
BuggyAsyncPipeline.cs
public abstract class DataPipeline
{
public void Run()
{
Extract(); // calls the override
Transform();
Load();
}
protected abstract void Extract();
protected abstract void Transform();
protected abstract void Load();
}
public class ApiDataPipeline : DataPipeline
{
private List<Record> _data = new();
// β async void β fire-and-forget! The pipeline doesn't wait!
protected override async void Extract()
{
var response = await _httpClient.GetAsync("https://api.example.com/data");
_data = await response.Content.ReadFromJsonAsync<List<Record>>();
// This completes AFTER Transform() and Load() have already run
}
protected override void Transform() { /* _data is empty! */ }
protected override void Load() { /* nothing to load! */ }
}
Why async void is dangerous: When a method returns void, the caller has no Task to await. The await inside the method pauses that method, but the caller (Run()) has already moved on to Transform(). The HTTP response arrives later, writes to _data, but by then Load() has already finished with an empty list.
FixedAsyncPipeline.cs
public abstract class DataPipeline
{
// β Template method is async and awaits each step
public async Task RunAsync()
{
await ExtractAsync();
await TransformAsync();
await LoadAsync();
}
protected abstract Task ExtractAsync();
protected abstract Task TransformAsync();
protected abstract Task LoadAsync();
}
public class ApiDataPipeline : DataPipeline
{
private List<Record> _data = new();
// β Returns Task β the pipeline properly awaits this
protected override async Task ExtractAsync()
{
var response = await _httpClient.GetAsync("https://api.example.com/data");
_data = await response.Content.ReadFromJsonAsync<List<Record>>();
}
protected override Task TransformAsync()
{
_data = _data.Select(Normalize).ToList();
return Task.CompletedTask; // sync steps return completed task
}
protected override async Task LoadAsync()
{
await _db.BulkInsertAsync(_data);
}
}
Lesson Learned
If any step in your template might be async, make the entire template method async. Define step methods as protected abstract Task StepAsync(). Synchronous implementations can simply return Task.CompletedTask. Never use async void except for event handlers — it's the #1 async pitfall in C#.
How to Spot This in Your Code
Search your codebase for async void — every hit that isn't an event handler is suspicious. The Roslyn analyzer VSTHRD100 ("Avoid async void methods") will flag these automatically. Another red flag: if an operation that should take hundreds of milliseconds (HTTP call, database write) completes in 0ms according to your logs, something is fire-and-forgetting. Check whether the step method's return type matches what the template method expects.
Bug 5: Hook with Side Effects
The Incident
Audit logging system. An OrderProcessor base class has a hook method OnBeforeProcess() that subclasses can optionally override. One subclass overrides it to write an audit log entry: "Order processing started." The problem? The hook runs before validation, so the audit log records "started" even for orders that immediately fail validation.
Worse: another subclass overrides the same hook to modify a shared counter field in the base class. Now the audit log count and the actual processed count are different, and the timing of hook calls creates phantom audit entries — the audit log shows 500 "started" entries but only 200 "completed" entries. Management panics: "Where did 300 orders go?"
What Went Wrong
A hook is supposed to be a gentle tap on the shoulder — "hey, something's about to happen, want to do anything?" But someone turned that gentle tap into a fire alarm that calls the fire department, writes a report, and updates a shared counter — all before anyone even checks if there's actually a fire. The hook ran before validation, so it logged "processing started" for orders that immediately failed. It's like a restaurant writing "meal served" in the logbook the moment someone sits down, before even checking if they have a reservation. The hook had real, observable side effects (database writes) that created phantom records that didn't match reality.
Time to Diagnose
1 day — the discrepancy between "started" and "completed" audit entries looked like a data loss bug. The team spent hours checking database transactions before realizing the audit log itself was the problem — it was recording events that hadn't actually happened yet.
BuggyHook.cs
public abstract class OrderProcessor
{
private int _processedCount = 0; // shared mutable state!
public void Process(Order order)
{
OnBeforeProcess(order); // hook β runs before validation
Validate(order); // might throw!
CalculateTotal(order);
ChargePayment(order);
OnAfterProcess(order);
_processedCount++;
}
// β Hooks that subclasses use for side effects
protected virtual void OnBeforeProcess(Order order) { }
protected virtual void OnAfterProcess(Order order) { }
}
public class AuditedOrderProcessor : OrderProcessor
{
// β Writes audit log BEFORE validation β creates phantom entries
protected override void OnBeforeProcess(Order order)
{
_auditLog.Write($"Order {order.Id} processing started");
}
}
FixedHook.cs
public abstract class OrderProcessor
{
public void Process(Order order)
{
Validate(order); // validate FIRST
OnValidationPassed(order); // β hook only fires after validation
CalculateTotal(order);
ChargePayment(order);
OnProcessingComplete(order); // β hook fires only on full success
}
// β Hooks placed AFTER their corresponding preconditions
protected virtual void OnValidationPassed(Order order) { }
protected virtual void OnProcessingComplete(Order order) { }
}
public class AuditedOrderProcessor : OrderProcessor
{
// β Audit log only records orders that actually passed validation
protected override void OnValidationPassed(Order order)
{
_auditLog.Write($"Order {order.Id} validated, processing started");
}
protected override void OnProcessingComplete(Order order)
{
_auditLog.Write($"Order {order.Id} completed successfully");
}
}
Lesson Learned
Hooks should be placed after their preconditions are met, not before. Name hooks to reflect when they fire: OnValidationPassed is much clearer than OnBeforeProcess. And avoid shared mutable state in the base class — if subclass hooks modify base fields, the order of operations creates subtle timing bugs that are nightmares to debug.
How to Spot This in Your Code
Look for any hook method named OnBefore... that writes to a database, sends an email, increments a counter, or touches shared state. If the hook runs before a step that can throw (like validation), you'll get phantom side effects. Also compare your audit/log counts against your actual processed counts — if "started" entries outnumber "completed" entries, a pre-validation hook with side effects is a likely suspect.
Bug 6: Too Many Abstract Methods
The Incident
Report generation framework. An architect designs a ReportGenerator base class with 12 abstract methods: ConnectToSource(), Authenticate(), FetchSchema(), FetchData(), ValidateData(), TransformData(), FormatHeaders(), FormatBody(), FormatFooters(), GenerateOutput(), SaveReport(), NotifyRecipients().
A new developer joins and creates SimpleLogReportGenerator. They need to override FetchData() and FormatBody() — that's it. But the compiler forces them to implement all 12 methods. They write empty stubs for the other 10, including Authenticate() (returns success without checking credentials) and ValidateData() (returns true for everything). Six months later, someone connects this report generator to a sensitive data source — no authentication, no validation.
What Went Wrong
Imagine a job application that asks you to fill in 12 fields, but you only care about 2 of them. You write "N/A" in the other 10 and submit. What if one of those fields was "Criminal background check: Pass/Fail?" and you wrote "Pass" without actually checking? That's what happened here. The base class forced every subclass to implement 12 abstract methods, even though most subclasses only needed 2 or 3. Developers filled the rest with do-nothing stubs that returned "success" — including security-critical methods like Authenticate() and ValidateData(). The stubs compiled without warnings, returned truthy values, and silently opened a security hole.
Time to Diagnose
6 months (!) — the security gap was only discovered during a penetration test. The empty Authenticate() stub meant anyone could generate reports from the sensitive data source without credentials. The stubs compiled cleanly and returned "success" values, so nothing failed visibly.
BuggyAbstractOverload.cs
public abstract class ReportGenerator
{
public void Generate()
{
ConnectToSource();
Authenticate();
FetchSchema();
// ... 9 more steps
}
// β ALL 12 steps are abstract β every subclass MUST implement all 12
protected abstract void ConnectToSource();
protected abstract bool Authenticate();
protected abstract void FetchSchema();
protected abstract List<object> FetchData();
protected abstract bool ValidateData(List<object> data);
protected abstract void TransformData();
protected abstract string FormatHeaders();
protected abstract string FormatBody();
protected abstract string FormatFooters();
protected abstract byte[] GenerateOutput();
protected abstract void SaveReport(byte[] output);
protected abstract void NotifyRecipients();
}
public class SimpleLogReportGenerator : ReportGenerator
{
// β Only 2 of 12 methods are actually meaningful
protected override List<object> FetchData() => ReadLogFiles();
protected override string FormatBody() => FormatLogs();
// β The other 10 are empty stubs to satisfy the compiler
protected override void ConnectToSource() { }
protected override bool Authenticate() => true; // β SECURITY HOLE
protected override bool ValidateData(List<object> data) => true; // β
// ... 7 more empty stubs
}
FixedAbstractOverload.cs
public abstract class ReportGenerator
{
public void Generate()
{
ConnectToSource();
Authenticate();
var data = FetchData();
ValidateData(data);
var output = FormatReport(data);
SaveReport(output);
OnReportGenerated(output); // optional hook
}
// β Only truly varying steps are abstract
protected abstract List<object> FetchData();
protected abstract string FormatReport(List<object> data);
// β Steps with sensible defaults are virtual (hooks)
protected virtual void ConnectToSource() { /* no-op for local sources */ }
protected virtual void Authenticate() { /* no-op for public data */ }
protected virtual void ValidateData(List<object> data)
{
if (data == null || data.Count == 0)
throw new InvalidOperationException("No data to report");
}
protected virtual void SaveReport(string output)
{
File.WriteAllText($"report_{DateTime.Now:yyyyMMdd}.txt", output);
}
protected virtual void OnReportGenerated(string output) { }
}
The fix: Only make abstract the steps that truly must differ for every subclass (fetching data and formatting). Everything else becomes a virtual hook with a sensible default. Subclasses override only what they need. No more empty stubs, no more accidental security holes.
Lesson Learned
Follow the "minimal abstract surface" rule: only force subclasses to implement what they must. Everything else should be a virtual hook with a safe default. If you have more than 3–4 abstract methods, reconsider your design — you might be pushing too much variation into a single class hierarchy.
How to Spot This in Your Code
Count the abstract methods in your base class. If there are more than 3 or 4, check how many of them are implemented as empty stubs or => true / => null in concrete subclasses. Also look for subclasses where more than half the overrides are one-liners returning default values — that's a sign the base class is asking too much. Pay special attention to stubs that return "success" booleans — those are the ones that silently bypass security checks.
Section 13
Pitfalls & Anti-Patterns
1. God Base Class
Mistake: Stuffing shared logic, utility methods, configuration, logging, error handling, and validation ALL into the abstract base class. It starts as a clean template and grows into a 2,000-line monster that every subclass depends on.
Why Bad: Every subclass inherits ALL of that baggage, even if it only needs one step. Changing anything in the base class risks breaking every subclass. The base becomes the most-changed file in the repo, and merge conflicts become a daily ritual.
Fix: Keep the base class thin — it should contain only the template method and the abstract/virtual step declarations. Move shared utilities into separate helper classes. Move cross-cutting concerns (logging, validation) into decorators or middleware. The base class is a skeleton, not a toolbox.
// Base class: ONLY the skeleton + step declarations
public abstract class DataProcessor
{
public void Process()
{
Validate();
Load();
Transform();
Save();
}
protected abstract void Validate();
protected abstract void Load();
protected abstract void Transform();
protected abstract void Save();
}
// Utilities live in their own classes
public static class RetryHelper { /* ... */ }
public class EmailValidator { /* ... */ }
2. Yo-Yo Problem
Mistake: Creating deep inheritance chainsWhen class A extends B, which extends C, which extends D, which extends E. Each level adds more behavior, but understanding any single class requires reading all the levels above it. This "tower" of inheritance is the Yo-Yo Problem β you bounce up and down the hierarchy trying to piece together what actually happens.: BaseProcessor → ValidatingProcessor → LoggingProcessor → RetryProcessor → MyActualProcessor. To understand what MyActualProcessor does, you have to bounce up and down between 5 files.
Why Bad: Debugging becomes a yo-yo — you read a method, see it calls base.DoSomething(), jump up one level, see another base.DoSomething(), jump up again. By the time you find the actual logic, you've lost track of where you started. New team members stare at the hierarchy in despair.
Fix: Keep inheritance to 2–3 levels maximum: abstract base → concrete implementation. If you need more customization layers, use composition (decorators, strategy injection) instead of more inheritance. A flat hierarchy is a debuggable hierarchy.
YoYoInheritance.cs
// 5 levels deep β good luck debugging
public abstract class BaseProcessor { /* skeleton */ }
public abstract class ValidatingProcessor
: BaseProcessor { /* adds validation */ }
public abstract class LoggingProcessor
: ValidatingProcessor { /* adds logging */ }
public abstract class RetryProcessor
: LoggingProcessor { /* adds retry logic */ }
// The class you actually use β inherits ALL the baggage
public class OrderProcessor
: RetryProcessor
{
// Which level handles what? Time to yo-yo...
protected override void Execute() => base.Execute();
}
FlatHierarchy.cs
// Flat: just base β concrete (2 levels)
public abstract class BaseProcessor
{
private readonly ILogger _logger; // compose extras
private readonly IRetryPolicy _retry;
public void Process()
{
Validate(); Load(); Transform(); Save();
}
protected abstract void Validate();
protected abstract void Load();
protected abstract void Transform();
protected abstract void Save();
}
public class OrderProcessor : BaseProcessor
{
protected override void Validate() { /* order rules */ }
protected override void Load() { /* fetch order */ }
protected override void Transform(){ /* apply discounts */ }
protected override void Save() { /* persist order */ }
}
3. Fragile Base Class
Mistake: Changing the base class in a way that seems harmless but breaks subclasses. For example, reordering steps in the template method, adding a new required step between existing ones, or changing the return type of a hook. This is the classic fragile base class problemA well-known OOP pitfall where changes to a base class unexpectedly break its subclasses. It happens because subclasses depend not just on the base class's interface, but on its implementation details β like step ordering, timing, and internal state. The more subclasses you have, the more fragile the base becomes..
Why Bad: Subclasses depend on the base class's behavior, not just its interface. If a subclass assumes step B runs after step A (and uses state set by A), swapping them silently breaks the subclass. The compiler won't catch it β it's a runtime logic bug.
Fix: Treat the template method's step order as a public contract. Document it. Write integration tests that verify step ordering. If you must change the skeleton, treat it like a breaking API change — update all subclasses and communicate the change to the team.
FragileReorder.cs
public abstract class ReportGenerator
{
public void Generate()
{
// v2: someone swapped Fetch and Validate
Validate(); // was #2, now #1
FetchData(); // was #1, now #2 β BREAKS subclasses!
Format();
Export();
}
protected abstract void FetchData();
protected abstract void Validate();
// ...
}
// Subclass assumed FetchData() ran BEFORE Validate()
public class SalesReport : ReportGenerator
{
private DataSet _data;
protected override void FetchData() => _data = LoadSales();
protected override void Validate() => Check(_data); // π₯ _data is null!
}
DocumentedOrder.cs
public abstract class ReportGenerator
{
/// <summary>
/// Step order contract: FetchData β Validate β Format β Export.
/// Changing order is a BREAKING CHANGE β update all subclasses.
/// </summary>
public void Generate()
{
FetchData(); // Step 1: always first
Validate(); // Step 2: data is available
Format(); // Step 3: data is clean
Export(); // Step 4: output is ready
}
}
// Integration test that enforces the contract
[Fact]
public void Steps_Execute_In_Documented_Order()
{
var log = new List<string>();
var sut = new TestReport(log);
sut.Generate();
Assert.Equal(new[] { "Fetch","Validate","Format","Export" }, log);
}
4. Forcing Unnecessary Overrides
Mistake: Making every step abstract when most subclasses only need to customize 1–2 steps. Developers end up writing empty stubs or copy-pasting default implementations just to satisfy the compiler.
Why Bad: Empty stubs are dangerous — they silently skip logic that might be important (see Bug 6 above). They also create maintenance burden: 20 subclasses each with 8 identical "default" implementations that should have been in the base.
Fix: Use abstract only for steps that must vary. Use virtual with sensible defaults for optional steps (hooks). The rule of thumb: if more than half your subclasses would implement the same thing, it should be a virtual method with that default behavior in the base.
public abstract class Notifier
{
public void Send()
{
Authenticate(); Validate(); Format();
AddHeader(); AddFooter(); Compress();
Encrypt(); Deliver();
}
// Only 2 MUST vary β these are abstract
protected abstract void Authenticate();
protected abstract void Deliver();
// 6 optional hooks β sensible defaults, override if needed
protected virtual void Validate() { /* basic null check */ }
protected virtual void Format() { /* plain text default */ }
protected virtual void AddHeader() { /* standard header */ }
protected virtual void AddFooter() { /* standard footer */ }
protected virtual void Compress() { /* no-op by default */ }
protected virtual void Encrypt() { /* no-op by default */ }
}
public class SimpleEmailNotifier : Notifier
{
protected override void Authenticate() => SmtpAuth();
protected override void Deliver() => SmtpSend();
// Done! No empty stubs needed.
}
5. Mixing Template Method with Strategy
Mistake: Using Template Method for the skeleton AND injecting Strategy objects for the same variation points. The subclass overrides a step, but the base class also accepts a strategy for that step via constructor injection. Now there are two ways to customize the same behavior, and it's unclear which one wins.
Why Bad: Developers don't know which customization mechanism to use. Some subclasses override the method, others inject a strategy, some do both (creating a conflict). The codebase becomes inconsistent and the "right way" depends on who you ask.
Fix: Pick one mechanism per variation point. If a step needs compile-time customization via subclassing, use Template Method. If it needs runtime flexibility, use Strategy. Don't offer both for the same step — it creates confusion without adding value.
MixedMechanisms.cs
public abstract class OrderPipeline
{
private readonly IValidator _validator; // Strategy injection
public OrderPipeline(IValidator validator)
=> _validator = validator;
public void Process()
{
Validate(); // which one runs?
Ship();
}
// Two ways to customize validation β confusing!
protected virtual void Validate()
=> _validator.Validate(); // strategy...
protected abstract void Ship();
}
public class ExpressOrder : OrderPipeline
{
public ExpressOrder(IValidator v) : base(v) { }
// ... AND also overrides β which one wins?
protected override void Validate()
=> CustomValidation(); // override ignores injected strategy!
protected override void Ship() => ExpressShip();
}
PickOneMechanism.cs
// Option A: Pure Template Method β override to customize
public abstract class OrderPipeline
{
public void Process()
{
Validate(); Ship();
}
protected abstract void Validate(); // one way: override
protected abstract void Ship();
}
// Option B: Pure Strategy β inject to customize
public class OrderPipeline
{
private readonly IValidator _validator;
private readonly IShipper _shipper;
public OrderPipeline(IValidator v, IShipper s)
{
_validator = v;
_shipper = s;
}
public void Process()
{
_validator.Validate(); // one way: injection
_shipper.Ship();
}
}
6. Mutable Shared State in Base
Mistake: The base class has mutable fields (counters, lists, flags) that template steps read and write. Step A writes to a field, step B reads it. Subclasses that override step A might forget to set the field, and step B blows up or silently uses stale data.
Why Bad: Shared mutable state creates invisible dependencies between steps. The order of step execution becomes load-bearing — if you ever reorder steps, move one to async, or skip one conditionally, the shared state falls out of sync. In multi-threaded scenarios, it's even worse: race conditions between concurrent template executions.
Fix: Pass data between steps via method parameters and return values, not shared fields. If steps need to share context, create an explicit context object that flows through the pipeline: var ctx = new PipelineContext(); Step1(ctx); Step2(ctx); Step3(ctx);. This makes dependencies visible and testable.
SharedMutableState.cs
public abstract class FileImporter
{
// Shared mutable fields β invisible step dependencies
protected string _rawContent;
protected List<Record> _parsed;
protected bool _isValid;
public void Import()
{
ReadFile(); // sets _rawContent
Parse(); // reads _rawContent, sets _parsed
Validate(); // reads _parsed, sets _isValid
Save(); // reads _isValid and _parsed
}
protected abstract void ReadFile();
protected abstract void Parse();
protected abstract void Validate();
protected abstract void Save();
}
// If subclass forgets to set _rawContent, Parse() blows up
// If steps run concurrently, fields get trampled
ExplicitContext.cs
public class ImportContext
{
public string RawContent { get; set; }
public List<Record> Parsed { get; set; }
public bool IsValid { get; set; }
}
public abstract class FileImporter
{
// No shared fields β context flows explicitly
public void Import()
{
var ctx = new ImportContext();
ReadFile(ctx); // must populate ctx.RawContent
Parse(ctx); // reads ctx.RawContent
Validate(ctx); // reads ctx.Parsed
Save(ctx); // reads ctx.IsValid
}
protected abstract void ReadFile(ImportContext ctx);
protected abstract void Parse(ImportContext ctx);
protected abstract void Validate(ImportContext ctx);
protected abstract void Save(ImportContext ctx);
}
// Dependencies are visible, testable, thread-safe
7. Not Sealing the Template Method
Mistake: Marking the template method as virtual, allowing subclasses to override the entire skeleton instead of just individual steps.
Why Bad: The whole point of Template Method is that the skeleton is fixed. If subclasses can replace it, they can skip steps, reorder them, or add steps in the wrong place. You lose all the guarantees the pattern provides (see Bug 2 for a real-world example of this going wrong).
Fix: In C#, simply don't add virtual to the template method. Make it a plain public method. The compiler will prevent subclasses from overriding it. In Java, use final. This is the single most important implementation detail of the pattern.
VirtualSkeleton.cs
public abstract class Checkout
{
// MISTAKE: virtual lets subclasses override the skeleton
public virtual void Process()
{
CalculateTotal();
ApplyDiscount();
ChargePayment();
SendReceipt();
}
protected abstract void CalculateTotal();
protected abstract void ApplyDiscount();
protected abstract void ChargePayment();
protected abstract void SendReceipt();
}
public class HackyCheckout : Checkout
{
// Subclass replaces entire skeleton β skips ChargePayment!
public override void Process()
{
CalculateTotal();
ApplyDiscount();
// "forgot" ChargePayment... free stuff! π₯
SendReceipt();
}
}
SealedSkeleton.cs
public abstract class Checkout
{
// Non-virtual: subclasses CANNOT override the skeleton
public void Process()
{
CalculateTotal();
ApplyDiscount();
ChargePayment(); // always runs β guaranteed
SendReceipt();
}
protected abstract void CalculateTotal();
protected abstract void ApplyDiscount();
protected abstract void ChargePayment();
protected abstract void SendReceipt();
}
public class ExpressCheckout : Checkout
{
// Can only customize individual steps, not the flow
protected override void CalculateTotal() => /* ... */;
protected override void ApplyDiscount() => /* ... */;
protected override void ChargePayment() => /* ... */;
protected override void SendReceipt() => /* ... */;
}
8. Copy-Paste "Customization"
Mistake: Instead of subclassing the base, a developer copies the entire base class, renames it, and tweaks a few lines. They now have two nearly-identical classes with no inheritance relationship.
Why Bad: Bug fixes in the original need to be manually copied to the clone. After a few months, the clones diverge and nobody knows which one is "correct." This is the exact problem Template Method was designed to solve — shared structure with customizable steps — and copy-pasting defeats the purpose entirely.
Fix: If someone feels the need to copy-paste the base class, that's a signal the base class is too rigid. Either add a hook method for the variation they need, or extract the shared logic into a proper base class they can extend. If the variation is too different for inheritance, consider switching to Strategy or composition.
CopyPasteClone.cs
// Original class
public class PdfExporter
{
public void Export()
{
LoadData();
FormatAsPdf();
AddWatermark();
SaveToFile();
}
// ... 80 lines of implementation
}
// "Customized" copy β 95% identical, 5% different
public class PdfExporterV2 // copy-pasted, renamed
{
public void Export()
{
LoadData();
FormatAsPdf();
AddWatermark();
AddPageNumbers(); // only real difference!
SaveToFile();
}
// ... same 80 lines duplicated, will drift over time
}
ProperSubclass.cs
public abstract class PdfExporter
{
public void Export()
{
LoadData();
FormatAsPdf();
AddWatermark();
AfterWatermark(); // hook for extensions
SaveToFile();
}
protected virtual void AfterWatermark() { } // no-op default
// ... shared implementation lives here ONCE
}
public class PageNumberedPdfExporter : PdfExporter
{
// Only the DIFFERENCE β no duplication
protected override void AfterWatermark()
=> AddPageNumbers();
}
Section 14
Testing Strategies
Testing Template Method requires a slightly different mindset than testing regular classes. You can't instantiate the abstract base directly, so you need to test through concrete subclasses or create test-specific subclasses. Here are four approaches, from most practical to last resort.
Test Concrete Subclasses Directly
The simplest and most practical approach: instantiate each concrete subclass, call the template method, and verify the final output. You're testing the whole pipeline — the skeleton from the base class plus the steps from the subclass — as a single unit. This is how you'd test in most real projects.
ConcreteSubclassTests.cs
public class PdfReportGeneratorTests
{
[Fact]
public void Generate_ValidData_ProducesPdfOutput()
{
// Arrange β real concrete subclass
var generator = new PdfReportGenerator(new InMemoryDataSource());
// Act β calls the template method (which calls all steps)
var result = generator.Generate();
// Assert β verify the final output
Assert.NotNull(result);
Assert.StartsWith("%PDF", result.Header); // valid PDF header
Assert.True(result.Pages.Count > 0);
}
[Fact]
public void Generate_EmptyData_ThrowsValidationException()
{
var generator = new PdfReportGenerator(new EmptyDataSource());
Assert.Throws<InvalidOperationException>(
() => generator.Generate());
}
}
Spy on Step Calls with a Test Subclass
Sometimes you need to verify that the skeleton calls steps in the right order. Create a test-only subclass that records which steps were called and in what sequence. This tests the base class's template method logic without depending on any real implementation.
StepOrderTests.cs
// Test-only subclass that records step calls
public class SpyReportGenerator : ReportGeneratorBase
{
public List<string> CalledSteps { get; } = new();
protected override List<object> FetchData()
{
CalledSteps.Add("FetchData");
return new List<object> { "test" };
}
protected override string FormatReport(List<object> data)
{
CalledSteps.Add("FormatReport");
return "formatted";
}
}
public class TemplateMethodOrderTests
{
[Fact]
public void Generate_CallsStepsInCorrectOrder()
{
var spy = new SpyReportGenerator();
spy.Generate();
// Verify the skeleton calls steps in the expected order
Assert.Equal(new[] { "FetchData", "FormatReport" }, spy.CalledSteps);
}
[Fact]
public void Generate_AlwaysCallsFetchBeforeFormat()
{
var spy = new SpyReportGenerator();
spy.Generate();
int fetchIndex = spy.CalledSteps.IndexOf("FetchData");
int formatIndex = spy.CalledSteps.IndexOf("FormatReport");
Assert.True(fetchIndex < formatIndex,
"FetchData must run before FormatReport");
}
}
Test Hooks Independently
Hook methods (optional virtual overrides) can have their own logic that deserves isolated testing. Create a minimal subclass that overrides just the hook you want to test, with all other steps returning simple defaults. This way you're testing the hook's behavior without interference from the rest of the pipeline.
HookTests.cs
// Minimal subclass β overrides only the hook under test
public class CustomValidationProcessor : OrderProcessorBase
{
public bool ValidationWasCalled { get; private set; }
public Order? LastValidatedOrder { get; private set; }
protected override void ValidateOrder(Order order)
{
ValidationWasCalled = true;
LastValidatedOrder = order;
// Custom validation: reject orders over $10,000
if (order.Total > 10_000m)
throw new ValidationException("Order exceeds limit");
}
// Other abstract steps β minimal implementations
protected override void ProcessPayment(Order o) { }
protected override void SendConfirmation(Order o) { }
}
public class ValidationHookTests
{
[Fact]
public void Process_LargeOrder_ThrowsValidationException()
{
var processor = new CustomValidationProcessor();
var bigOrder = new Order { Total = 15_000m };
Assert.Throws<ValidationException>(
() => processor.Process(bigOrder));
}
[Fact]
public void Process_NormalOrder_PassesValidation()
{
var processor = new CustomValidationProcessor();
var normalOrder = new Order { Total = 50m };
processor.Process(normalOrder);
Assert.True(processor.ValidationWasCalled);
}
}
Integration Test the Full Pipeline
When the template method orchestrates real external systems (database, HTTP APIs, file system), you may need an end-to-end test that runs the entire pipeline against real (or docker-based) dependencies. This verifies everything works together, but it's slow and fragile.
IntegrationTests.cs
[Collection("Database")]
public class SqlReportIntegrationTests : IAsyncLifetime
{
private readonly TestDatabase _db = new();
public async Task InitializeAsync() => await _db.SeedTestData();
public async Task DisposeAsync() => await _db.Cleanup();
[Fact]
public async Task Generate_WithRealDatabase_ProducesCompleteReport()
{
var generator = new SqlReportGenerator(_db.ConnectionString);
var report = await generator.GenerateAsync();
Assert.NotEmpty(report.Rows);
Assert.Equal("PDF", report.Format);
Assert.True(File.Exists(report.OutputPath));
}
}
Last Resort
Integration tests are expensive to write, slow to run, and brittle. Use them sparingly — only when the interaction between steps and real systems is the thing you need to verify. For most Template Method testing, strategies 1–3 are sufficient and much faster.
Section 15
Performance Considerations
Template Method is one of the lightest patterns in terms of runtime cost. There's no extra indirection layer, no wrapper objects, no runtime lookup tables — it's just inheritance and virtual method calls. That said, there are a few things worth knowing.
Virtual Call Overhead
Each step in the template method is a virtual method callWhen the runtime calls a virtual method, it looks up the actual implementation through a pointer table (vtable). This takes a tiny bit longer than a direct call because the CPU can't know in advance which method will run. In practice, modern CPUs predict virtual calls very well, so the overhead is typically 1–3 nanoseconds.. The CPU needs to look up which concrete implementation to run via the vtable. This adds a tiny overhead compared to a direct (non-virtual) call.
Call Type
Time (approx)
Overhead vs Direct
When It Matters
Direct method call
~0.5 ns
baseline
Always fast
Virtual method call
~1–3 ns
+1–2 ns
Negligible for most apps
Interface dispatch
~2–5 ns
+2–4 ns
Still negligible
Template with 5 virtual steps
~5–15 ns total
+5–10 ns
Only matters in tight loops (10M+ calls/sec)
Mitigation
For 99.9% of applications, virtual call overhead is irrelevant. If a step does any I/O (database, HTTP, file), the virtual call cost is less than 0.001% of the step's total time. Only optimize if profiling shows the template method is a genuine hotspot called millions of times in a tight loop.
Inheritance Chain Depth
Deep inheritance hierarchies (4+ levels) can slow down method resolution slightly because the runtime walks up the chain to find the correct override. More importantly, deep chains hurt JIT compilation — the just-in-time compiler is less likely to inline methods when the call chain is deep and branches are unpredictable.
The real cost isn't nanoseconds, though — it's developer time. Deep chains make code harder to understand, debug, and modify. The performance cost and the maintenance cost both push in the same direction: keep it shallow.
Mitigation
Limit inheritance to 2–3 levels: abstract base → concrete implementation (and maybe one intermediate abstract class for shared variants). If you need more customization depth, switch to composition — inject strategy objects for the varying parts instead of adding more inheritance layers.
Allocation in Step Methods
Template Method itself doesn't allocate anything extra — there are no wrapper objects or strategy delegates. But if individual step methods create temporary objects (strings, lists, DTOs) on every call, GC pressureGarbage Collection pressure happens when your code allocates many short-lived objects. The .NET garbage collector must periodically pause your application to clean them up. High allocation rates (thousands of objects per second) in hot paths can cause noticeable latency spikes, especially in Gen 0 collections. builds up in hot paths.
AllocationMitigation.cs
// β BAD: Allocates new list + string on every call
protected override string FormatReport(List<object> data)
{
var lines = new List<string>(); // allocation!
foreach (var item in data)
lines.Add($"Row: {item}"); // string allocation per row!
return string.Join("\n", lines); // another allocation!
}
// β GOOD: Reuse StringBuilder, avoid per-item allocations
private readonly StringBuilder _sb = new();
protected override string FormatReport(List<object> data)
{
_sb.Clear(); // reuse, no allocation
foreach (var item in data)
_sb.Append("Row: ").Append(item).AppendLine();
return _sb.ToString(); // one allocation total
}
For hot-path template methods (called thousands of times per second), use object pooling, Span<T>, ArrayPool<T>, and StringBuilder reuse to minimize allocations. For normal business logic methods, don't worry about it — clarity beats micro-optimization.
Section 16
How to Explain in an Interview
Your Script (90 Seconds)
Opening: "Template Method is about defining a recipe where the steps are fixed, but the ingredients can change. The base class says: 'First we do A, then B, then C' — and subclasses fill in what A, B, and C actually do."
Core: "The base class owns the algorithm skeleton — it calls the steps in order. Subclasses override specific steps to customize behavior, but they can't change the order or skip steps. The template method itself is non-virtual, so the skeleton is locked. Only the individual steps are overridable."
Example: "Think of BackgroundService in .NET — the framework handles start, stop, and lifecycle management. You just override ExecuteAsync to define what your service does. The framework guarantees the lifecycle steps run in order; you just fill in the custom part."
When: "I use it when multiple classes share the same workflow but differ in details — like different report formats that all follow gather-data, format, write. Or different checkout flows that all validate, charge, confirm but each step works differently."
Close: "The key tradeoff is inheritance — it's compile-time. If you need runtime flexibility, Strategy is better. But for framework design where you control the skeleton and users customize the steps, Template Method is perfect. It's the Hollywood Principle in action: 'don't call us, we'll call you.'"
Section 17
Interview Q&As
29 questions that interviewers actually ask about Template Method — from the basics all the way to production-level design decisions. Each has a "Think First" prompt (try answering before peeking!) and a "Great Answer Bonus" to help you stand out.
Easy Foundations (Q1—Q5)
Q1: What is the Template Method pattern in simple terms?
Easy
Think First Think of a recipe that always has the same steps (prep, cook, serve) but the actual food changes. How does that map to code?
Template Method is a pattern where a base class defines the overall structure of an algorithm — which steps run and in what order — but lets subclasses fill in the details of specific steps. The base class says "do A, then B, then C." Subclasses decide what A, B, and C actually do.
Think of a cooking class. The instructor says: "First, prep your ingredients. Then cook them. Then plate and serve." Every student follows the same three steps in the same order — but one student makes pasta, another makes stir-fry, and a third makes soup. The structure is fixed (prep → cook → serve); the content varies.
In code, the base class has a non-virtual method (the "template method") that calls abstract or virtual methods in sequence. Subclasses override those step methods. The template method itself can't be overridden, ensuring the algorithm structure stays intact.
Great Answer Bonus "Template Method embodies the Hollywood Principle: 'Don't call us, we'll call you.' The base class controls the flow and calls subclass methods when needed — the subclass never calls the template method steps directly."
Q2: What's the difference between an abstract method and a hook method?
Easy
Think First One type forces subclasses to implement it. The other is optional. Which is which?
Two kinds of "customizable steps" in a Template Method:
Abstract method — a step that must be implemented by every subclass. The base class declares it with abstract, provides no default, and the compiler forces subclasses to fill it in. Example: protected abstract string FormatReport(List<object> data);
Hook method — a step that can be overridden but doesn't have to be. The base class declares it with virtual and provides a default implementation (often empty or a simple no-op). Subclasses override it only if they need to. Example: protected virtual void OnReportGenerated() { }
The general rule: make steps abstract when every subclass must provide its own implementation (the template can't work without it). Make steps virtual hooks when they're optional extension points that most subclasses can safely ignore.
Great Answer Bonus "A common mistake is making too many methods abstract. If more than half your subclasses end up implementing the same default, that default should live in the base class as a virtual hook, not be duplicated across every subclass."
Q3: Give a real-world analogy for Template Method.
Easy
Think First Think of a process with fixed steps where the details change every time — like building a house.
Building a house. Every house follows the same high-level process: lay the foundation → build the frame → add the roof → install plumbing/electrical → finish the interior. That sequence never changes — you can't install plumbing before the frame exists. But the details of each step vary wildly: a bungalow has a different frame than a two-story colonial, a modern house has different plumbing than a farmhouse.
The city's building code is the "template method" — it mandates which steps happen and in what order. The architect and builders are the "subclasses" — they customize each step within the rules. The building code doesn't care whether you're building a cabin or a mansion, as long as you follow the sequence.
Great Answer Bonus "Other good analogies: tax filing (same forms, different numbers), onboarding new employees (same steps, different roles), or a university degree (same course structure, different electives)."
Q4: Why should the template method be sealed/final?
Easy
Think First What happens if a subclass overrides the template method itself (not just the steps)?
If the template method is virtual, a subclass can override the entire skeleton — skipping steps, reordering them, or replacing the algorithm completely. This defeats the whole purpose of the pattern. The pattern's core guarantee is: "these steps always run in this order." If subclasses can bypass that, you have no guarantee at all.
In C#, you seal the template method by simply not marking it virtual. A non-virtual public method cannot be overridden. In Java, you use the final keyword. This way, the compiler enforces the contract — subclasses cannot break the skeleton, only customize individual steps.
Great Answer Bonus "I once saw a production bug where a developer accidentally overrode the template method in a checkout processor, skipping the payment step. $47K in unprocessed payments before anyone noticed. Since then, I always make template methods non-virtual."
Q5: Name one .NET class that uses Template Method.
Easy
Think First Think of a .NET base class where you override one method and the framework handles the rest of the lifecycle.
BackgroundService (in Microsoft.Extensions.Hosting) is the most commonly used example. The framework handles the full service lifecycle: starting, stopping, cancellation token management, and graceful shutdown. You just override ExecuteAsync(CancellationToken) to define what your background service actually does. The "template method" is the internal StartAsync/StopAsync lifecycle that you never touch.
Other .NET examples: Stream (with Read/Write as the customizable steps), HttpMessageHandler (override SendAsync), ControllerBase in ASP.NET (the action method pipeline), and DbContext.OnModelCreating() in EF Core (the framework calls it during model building).
Great Answer Bonus "BackgroundService is interesting because it also demonstrates the async Template Method pattern — the template returns Task and the step method is async. This is the modern way to do Template Method when I/O is involved."
Medium Applied Knowledge (Q6—Q12)
Q6: Template Method vs Strategy — when to use which?
Medium
Think First One uses inheritance, the other uses composition. Which gives runtime flexibility?
The decision comes down to what varies and when it varies:
Template Method — Use when the algorithm structure is fixed and only individual steps change. The variation is known at compile time (you pick a subclass). Best for frameworks where you control the workflow and users customize specific steps.
Strategy — Use when the entire algorithm is interchangeable and you need to swap it at runtime. The variation is injected via composition (pass a different strategy object). Best for business logic where behavior depends on configuration, user input, or runtime conditions.
Rule of thumb: If you're saying "always do A then B then C, but each step varies" → Template Method. If you're saying "the whole approach changes depending on the situation" → Strategy.
Great Answer Bonus "You can actually combine them. Use Template Method for the skeleton, and inject Strategy objects for specific steps that need runtime flexibility. For example, a report generator with a fixed pipeline (Template Method) but pluggable formatters (Strategy for the format step)."
Q7: How does Template Method relate to the Hollywood Principle?
Medium
Think First "Don't call us, we'll call you." Who is "us" and who is "you" in Template Method?
The Hollywood Principle says: "Don't call us, we'll call you." In Template Method, the base class is Hollywood (the studio), and the subclass is the actor. The subclass doesn't decide when its methods run — it just provides the implementations and waits. The base class's template method calls the subclass's overridden methods at the right time, in the right order.
This is a form of Inversion of Control (IoC). Instead of the subclass calling base class methods, the base class controls the flow and calls down into the subclass. The subclass never invokes the template method steps directly — it just fills them in. This inverted control flow is exactly what frameworks do: you provide the customization, the framework calls your code when it's time.
Great Answer Bonus "This is the same principle behind ASP.NET's middleware pipeline, BackgroundService, and dependency injection in general. The framework controls the lifecycle; your code plugs into extension points. Template Method is one of the oldest and simplest implementations of this idea."
Q8: Can Template Method work with interfaces instead of abstract classes?
Medium
Think First Can an interface in C# have a method with a body that calls other interface methods?
Traditionally, no — Template Method requires a method with a body (the skeleton) that calls abstract methods. Interfaces couldn't have method bodies, so abstract classes were the only option.
But since C# 8 / .NET Core 3.0, interfaces support default interface methods (DIMs). This means you can technically put a template method in an interface:
InterfaceTemplate.cs
public interface IReportGenerator
{
// Template method with default body
void Generate()
{
var data = FetchData();
var formatted = FormatReport(data);
SaveReport(formatted);
}
List<object> FetchData(); // must implement
string FormatReport(List<object> d); // must implement
void SaveReport(string output) { } // optional hook with default
}
However, this is rarely done in practice. The template method in an interface can't be sealed (implementations can override it), you lose the ability to store shared state, and the pattern feels unnatural. Stick with abstract classes for Template Method — that's what they're designed for.
Great Answer Bonus "Default interface methods are better suited for adding backward-compatible API extensions to existing interfaces, not for Template Method. The abstract class approach gives you sealed template methods, shared fields, constructor validation, and clear intent."
Q9: How do you test a Template Method implementation?
Medium
Think First You can't instantiate the abstract base class. So how do you test the skeleton logic?
Four approaches, from most to least practical:
Test concrete subclasses directly — instantiate PdfReportGenerator, call Generate(), verify the output. This tests the skeleton + steps together as a unit.
Create a spy/test subclass — make a test-only subclass that records which steps were called and in what order. This isolates the skeleton's control flow.
Test hooks independently — create a minimal subclass that overrides just the hook you care about, with trivial defaults for everything else.
Integration tests (last resort) — test the full pipeline against real dependencies when step interactions matter.
The spy subclass approach is especially valuable for verifying that the template method calls steps in the correct order and handles exceptions properly (e.g., if step 2 throws, does cleanup still run?).
Great Answer Bonus "I avoid mocking frameworks like Moq for this because they can't easily mock abstract class constructors. A hand-written spy subclass is simpler, more readable, and gives you full control over what each step returns or throws."
Q10: What is the Yo-Yo problem and how does it relate to Template Method?
Medium
Think First What happens when you have 5 levels of inheritance and need to debug a method call?
The Yo-Yo problem happens when you have deep inheritance chains and trying to understand the code requires bouncing up and down between parent and child classes — like a yo-yo on a string. You read a method in class D, it calls base.DoWork(), so you jump to class C. That also calls base.DoWork(), so you jump to class B. And so on.
Template Method is particularly susceptible to this because the base class calls down to the subclass (step methods), and the subclass might call back up (base.SomeHook()). With multiple inheritance levels, you're constantly jumping between files trying to piece together what actually executes.
Prevention: Keep inheritance shallow (2–3 levels max). If a step needs further customization, use composition (inject a strategy) rather than adding another inheritance layer.
Great Answer Bonus "The Yo-Yo problem is why some teams prefer Strategy over Template Method even when the skeleton is fixed. With Strategy, all the code is in one place — no jumping between files. The tradeoff is losing the locked skeleton guarantee."
Q11: How does BackgroundService use Template Method?
Medium
Think First What methods does BackgroundService provide, and which one do you override?
BackgroundService implements IHostedService, which the .NET hosting framework calls during application startup and shutdown. The template method is essentially the lifecycle: the framework calls StartAsync(), which internally calls your ExecuteAsync() override, and later calls StopAsync() for graceful shutdown.
The key methods:
StartAsync(CancellationToken) — sealed-ish (you can override but shouldn't). Starts the background task and stores the Task reference.
ExecuteAsync(CancellationToken) — the abstract step you override. This is where your actual work goes.
StopAsync(CancellationToken) — triggers cancellation and awaits the task. The framework handles graceful shutdown.
You never call StartAsync or StopAsync yourself — the framework does. You just fill in ExecuteAsync. That's the Hollywood Principle at work.
Great Answer Bonus "BackgroundService also shows why Template Method and async work well together. The framework awaits your ExecuteAsync, handles CancellationToken propagation, and manages the Task lifecycle. If you used raw IHostedService, you'd have to handle all that boilerplate yourself."
Q12: Can you combine Template Method with other patterns?
Medium
Think First What if one step of the template needs runtime flexibility? What pattern would you inject?
Absolutely — Template Method combines well with several patterns:
Template Method + Strategy: The skeleton is fixed (Template Method), but one or more steps delegate to an injected strategy object. This gives you a locked workflow with runtime-swappable steps.
Template Method + Factory Method: One of the template steps creates an object. The subclass overrides that step to create a different concrete type. Factory Method is essentially a specialized Template Method.
Template Method + Observer: The template fires events (hooks) at key points. Observers subscribe to those events instead of subclassing. This decouples the notification from the algorithm.
Template Method + Decorator: Wrap the concrete subclass in decorators to add cross-cutting concerns (logging, caching) without modifying the template or its steps.
Great Answer Bonus "The Template Method + Strategy combo is my go-to. It gives the best of both worlds: a guaranteed step order (Template Method) with pluggable step implementations (Strategy). In DI, I register the strategies and let the container inject them into the base class constructor."
Hard Expert Scenarios (Q13—Q29)
Q13: How would you refactor a switch statement into Template Method?
Hard
Think First You have a method with a switch on "type" that runs similar workflows with slight differences. How do you eliminate the switch?
The classic scenario: a method with a switch statement where each case runs a similar sequence of steps with minor variations. Here's the refactoring process:
Identify the common skeleton — look at the switch cases. What steps are the same across all cases? That's your template method.
Identify the varying steps — which lines change between cases? Those become abstract or virtual methods.
Create the base class — move the common skeleton into a non-virtual template method. Declare abstract methods for the varying steps.
Create subclasses — one per switch case. Each subclass implements the varying steps for its specific case.
Replace the switch with polymorphism — use a factory or DI to create the right subclass based on the type, then call the template method.
Switch to Template Method
// BEFORE: switch-based
void Export(string format, Data data)
{
Validate(data); // same for all
switch (format)
{
case "pdf": FormatAsPdf(data); break; // varies
case "csv": FormatAsCsv(data); break; // varies
case "xlsx": FormatAsExcel(data); break; // varies
}
SaveToFile(data); // same for all
}
// AFTER: Template Method
abstract class DataExporter
{
public void Export(Data data) // sealed skeleton
{
Validate(data);
Format(data); // polymorphic step
SaveToFile(data);
}
protected abstract void Format(Data data);
}
class PdfExporter : DataExporter
{
protected override void Format(Data data) { /* PDF logic */ }
}
// CsvExporter, ExcelExporter similarly...
Great Answer Bonus "The switch-to-polymorphism refactoring is one of the most common uses of Template Method. It also opens the door to OCP — adding a new format means adding a new class, not modifying the switch statement."
Q14: Template Method vs NVI (Non-Virtual Interface) — what's the difference?
Hard
Think First NVI says "public methods should be non-virtual, virtual methods should be non-public." How does that relate to Template Method?
The Non-Virtual Interface (NVI) pattern is essentially Template Method applied at the method level. The idea: public methods are non-virtual (they define the contract and can add pre/post processing), and virtual methods are protected (they're the customization points).
The relationship:
Template Method — a specific GoF pattern about defining algorithm skeletons with customizable steps. It's a design pattern applied to a specific problem (multi-step algorithms).
NVI — a general coding guideline that says every public method should be a mini template method. The public method handles invariants (validation, logging, locking), then delegates to a protected virtual method for the actual work.
In practice, NVI is Template Method scaled down to individual methods. Every public method in a well-designed base class follows this pattern: public non-virtual wrapper → protected virtual implementation.
Great Answer Bonus "C++'s Herb Sutter popularized NVI. In C#, it's the reason you see patterns like public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } with protected virtual void Dispose(bool disposing). The public method enforces the protocol; the virtual method provides the customization."
Q15: How do you handle async steps in a Template Method?
Hard
Think First What goes wrong if you mix sync and async in a template method?
Three rules for async Template Method:
Make the template method itself async — public async Task ProcessAsync(). It must return Task so the caller can await it.
Make all step methods return Task — protected abstract Task ValidateAsync(). Even steps that are synchronous should return Task.CompletedTask. This keeps the API uniform.
Never use async void — it detaches from the execution flow. The template method won't wait for the step, exceptions won't propagate, and you get silent failures.
AsyncTemplateMethod.cs
public abstract class AsyncPipeline
{
public async Task RunAsync(CancellationToken ct = default)
{
await InitializeAsync(ct);
var data = await FetchDataAsync(ct);
var result = await ProcessAsync(data, ct);
await SaveAsync(result, ct);
await CleanupAsync(ct); // always runs (or use try/finally)
}
protected virtual Task InitializeAsync(CancellationToken ct)
=> Task.CompletedTask; // optional hook
protected abstract Task<Data> FetchDataAsync(CancellationToken ct);
protected abstract Task<Result> ProcessAsync(Data d, CancellationToken ct);
protected abstract Task SaveAsync(Result r, CancellationToken ct);
protected virtual Task CleanupAsync(CancellationToken ct)
=> Task.CompletedTask; // optional hook
}
Great Answer Bonus "I always pass CancellationToken through all async steps. This allows graceful cancellation at any point in the pipeline. BackgroundService does exactly this — the cancellation token flows from StopAsync through to your ExecuteAsync."
Q16: What happens if a base class constructor calls a virtual method?
Hard
Think First In C#, which constructor runs first — the base or the derived?
The base constructor runs before the derived constructor. So if the base constructor calls a virtual method, and the derived class overrides it, the override runs — but the derived class's fields haven't been initialized yet (they're still null/default). This leads to NullReferenceException or incorrect behavior.
The execution order is:
Derived field initializers run (but NOT constructor body)
Base constructor body runs (calls virtual method → derived override runs)
Derived constructor body runs (too late!)
The fix: never call virtual methods from constructors. Move initialization into the template method itself or provide a separate Initialize() step that runs after construction is complete.
Great Answer Bonus "The C# analyzer CA2214 flags this. Also note that C# differs from Java here — in Java, field initializers run before the constructor call, but the virtual dispatch still happens on an uninitialized object. Both languages agree: don't call virtual methods from constructors."
Q17: How does Template Method interact with DI containers?
Hard
Think First DI containers resolve interfaces. Template Method uses abstract classes. How do you bridge the gap?
Template Method and DI have a slightly awkward relationship because DI favors interface-based composition while Template Method uses inheritance. Here's how to make them work together:
Register concrete subclasses directly — services.AddScoped<ReportGenerator, PdfReportGenerator>();. The DI container creates the subclass, injecting any constructor dependencies.
Inject dependencies into the base class constructor — the base class declares constructor parameters for shared dependencies (logger, config). Subclasses call : base(logger, config).
Hybrid approach: Template + Strategy via DI — the base class accepts strategy interfaces in its constructor. DI injects the right strategy implementation. The template method calls the injected strategies for specific steps.
The hybrid approach is the most DI-friendly because it reduces the inheritance depth and makes step implementations independently testable.
Great Answer Bonus "For cases where multiple subclasses need to be resolved dynamically, I use a factory pattern: register a Func<string, ReportGenerator> factory that maps format names to concrete subclasses. The consumer asks the factory for the right generator."
Q18: How would you implement a Template Method that supports cancellation?
Hard
Think First How do you let a caller say "stop processing" partway through the pipeline?
Pass a CancellationToken through the template method and check it between (or within) steps:
Pass the token to every step — protected abstract Task StepAsync(CancellationToken ct). Steps that do I/O pass it to HttpClient, DbCommand, etc.
Check cancellation between steps — call ct.ThrowIfCancellationRequested() between expensive steps in the template method.
Use try/finally for cleanup — even if cancellation throws OperationCanceledException, the cleanup step should still run.
CancellableTemplate.cs
public abstract class CancellablePipeline
{
public async Task RunAsync(CancellationToken ct)
{
try
{
await InitAsync(ct);
ct.ThrowIfCancellationRequested();
await ProcessAsync(ct);
ct.ThrowIfCancellationRequested();
await FinalizeAsync(ct);
}
finally
{
await CleanupAsync(); // always runs, even on cancel
}
}
}
Great Answer Bonus "I follow the same pattern as ASP.NET Core's request pipeline: token flows through every layer, and each step is responsible for checking it. For long-running steps, I check inside loops too, not just between steps."
Q19: Explain the Fragile Base Class problem in context of Template Method.
Hard
Think First What happens when you change the base class and 15 subclasses break?
The Fragile Base Class problem is when changes to the base class silently break subclasses. In Template Method, this is especially dangerous because subclasses depend on the behavior of the base (step ordering, state mutations, preconditions) — not just its interface.
Three common ways it manifests:
Reordering steps — a subclass assumes step A runs before step B (and uses state set by A). Swapping A and B breaks the subclass without any compiler error.
Adding a new required step — inserting a new abstract method means ALL existing subclasses must implement it. With 20 subclasses, that's 20 files to update.
Changing a hook's default behavior — subclasses that relied on the old default (without overriding it) suddenly behave differently.
Mitigations: Write integration tests that verify step order. Treat the skeleton as a public API — changes are breaking changes. Use virtual hooks (with defaults) instead of abstract methods for optional steps, so adding a new step doesn't force all subclasses to update.
Great Answer Bonus "This is why Template Method trades flexibility for reliability. The tight coupling between base and subclass is the cost. For systems that change frequently, I might use Strategy + composition instead, because changes to one strategy don't ripple to others."
Q20: How do you version a Template Method without breaking existing subclasses?
Hard
Think First You need to add a new step to the template. How do you avoid forcing 20 subclasses to change?
Several strategies for backward-compatible evolution:
Add new steps as virtual hooks with defaults — instead of abstract, use virtual with a no-op or sensible default. Existing subclasses don't need to change; new ones can override if they need the new step.
Use the "sandwich" pattern — wrap the new step in a virtual method that defaults to calling the old behavior. This lets subclasses opt into the new behavior without breaking the old.
Create a new base class — ReportGeneratorV2 : ReportGeneratorBase with the new step. Old subclasses extend V1, new ones extend V2. Both V1 and V2 extend the same root.
Feature flag in the template — the new step is guarded by if (UseNewValidation). Subclasses opt in by setting the flag.
Option 1 is the safest and most common. The key insight: virtual with a default is always safer than abstract when evolving a template, because abstract forces all subclasses to change immediately.
Great Answer Bonus "This is the same principle behind default interface methods in C# 8 — adding new methods with defaults to an interface so existing implementations don't break. The pattern works for abstract classes too."
Q21: Template Method in functional programming — how would you do it without inheritance?
Hard
Think First Functions are first-class in FP. How do you pass "steps" to a template?
In functional programming, you don't need inheritance at all. The template is a higher-order function that accepts step functions as parameters. Instead of subclassing to override steps, you pass different functions for the parts that vary.
FunctionalTemplateMethod.cs
// Functional Template Method in C#
static async Task ExportData(
Func<Task<List<Record>>> fetch,
Func<List<Record>, string> format,
Func<string, Task> save)
{
var data = await fetch(); // step 1
if (data.Count == 0) return; // skeleton logic
var output = format(data); // step 2
await save(output); // step 3
}
// Usage β no classes needed!
await ExportData(
fetch: () => db.QueryAsync<Record>("SELECT * FROM Orders"),
format: records => JsonSerializer.Serialize(records),
save: json => File.WriteAllTextAsync("export.json", json)
);
The skeleton logic (null check, step ordering) lives in the higher-order function. The varying steps are passed as lambda arguments. This is essentially Strategy pattern combined with a fixed orchestrator — the functional equivalent of Template Method.
Great Answer Bonus "This approach is common in modern C# even in OOP codebases. LINQ's pipeline (Where, Select, OrderBy) is a functional template — the pipeline structure is fixed, and you pass lambdas for the varying parts."
Q22: How does ASP.NET Core middleware relate to Template Method?
Hard
Think First Middleware has a fixed pipeline structure. Each middleware decides to call next() or not. Which pattern is that closer to?
ASP.NET Core middleware is a hybrid of Template Method and Chain of Responsibility, but it's closer to Template Method in spirit. The framework defines the pipeline structure: authentication runs before authorization, which runs before routing, which runs before your endpoint. You customize what happens at each stage, but the framework controls the order.
The similarity to Template Method:
The framework owns the skeleton — the middleware pipeline is configured at startup and the order is fixed.
You fill in the steps — each middleware is a customization point in the pipeline.
The Hollywood Principle applies — the framework calls your middleware, not the other way around.
The key difference: middleware is composition-based (each middleware is an independent component that calls next()), not inheritance-based. It's Template Method's concept implemented with a different mechanism.
Great Answer Bonus "The middleware pipeline shows how Template Method's concept evolved beyond inheritance. Modern frameworks prefer composition pipelines over class hierarchies, but the core idea — fixed skeleton with pluggable steps — is the same."
Q23: Design a data pipeline using Template Method — what are the steps?
Hard
Think First ETL (Extract, Transform, Load) is a natural fit. What other steps does a production pipeline need?
A production-grade ETL data pipeline using Template Method:
Initialize (hook) — open connections, create temp directories
Validate Source (hook) — check that the data source is reachable and has data
Extract (abstract) — read data from source (SQL, API, file, message queue)
Transform (abstract) — clean, normalize, map the data
Cleanup (hook, in finally block) — close connections, delete temp files
Steps 3, 4, and 6 are abstract because they vary for every data source/destination. Steps 1, 2, 5, 7, and 8 are virtual hooks with sensible defaults — most pipelines need them but the default behavior is usually fine.
Great Answer Bonus "I'd also add a CancellationToken flowing through all steps and a progress callback hook so callers can monitor long-running imports. For the template method itself, I'd wrap steps in try/catch to ensure Cleanup always runs and failed steps are logged with the step name."
Q24: How do you prevent subclasses from breaking the template's invariants?
Hard
Think First A subclass returns null from a step that the next step expects non-null. How do you guard against this?
Multiple defense layers:
Don't make the template method virtual — prevents subclasses from replacing the skeleton entirely.
Validate step outputs in the template — after calling a step, check its return value before passing it to the next step. var data = FetchData(); ArgumentNullException.ThrowIfNull(data);
Use the NVI pattern — the public method validates; the protected virtual method provides the implementation. The subclass can't skip the validation.
Make preconditions explicit — document what each step must return and what state it must maintain. Better yet, express it in code with guard clauses.
Write contract tests — test that every concrete subclass satisfies the base class's expectations (non-null returns, valid state transitions, etc.).
InvariantProtection.cs
public void Process(Order order)
{
// Template method with invariant checks
Validate(order);
var total = CalculateTotal(order);
if (total < 0) throw new InvalidOperationException(
$"CalculateTotal returned {total} β must be >= 0");
ChargePayment(order, total);
SendConfirmation(order);
}
Great Answer Bonus "In mission-critical systems, I add assertions between steps that verify postconditions. These act as runtime contracts — if a subclass violates the invariant, we fail fast with a clear error instead of propagating corrupt state."
Q25: What are the SOLID implications of Template Method?
Hard
Think First Go through each SOLID principle and ask: does Template Method help or hinder?
A quick principle-by-principle assessment:
SRP (Supports) — Each subclass handles one variant. The base class handles shared structure. Clear separation of concerns.
OCP (Supports) — Add new variants by creating new subclasses. No modification to the base class or existing subclasses needed.
LSP (Depends) — Subclasses must honor the base class contract. If a step returns unexpected types or throws unexpected exceptions, LSP breaks. The template's invariants are the contract.
ISP (Depends) — If the base forces too many abstract methods, subclasses must implement steps they don't need. This violates ISP. Solution: use hooks with defaults instead of excessive abstract methods.
DIP (Caution) — Subclasses depend on the concrete base class, not an abstraction. This tight coupling is the main tradeoff. Strategy pattern is more DIP-friendly because it depends on interfaces.
Bottom line: Template Method is good for SRP and OCP, neutral on LSP and ISP (depends on your design), and slightly problematic for DIP. It's a tradeoff: you get a locked skeleton but pay with tight inheritance coupling.
Great Answer Bonus "The DIP tension is why many modern codebases evolve from Template Method to Strategy over time. You start with Template Method for its simplicity, then refactor to Strategy when you need looser coupling or runtime flexibility."
Q26: How would you migrate from Template Method to Strategy? When would you?
Hard
Think First What triggers the migration? What does the end state look like?
When to migrate:
You need to swap algorithms at runtime (configuration, feature flags, A/B testing)
The inheritance hierarchy is getting deep (3+ levels — Yo-Yo problem)
You need to combine step implementations from different subclasses (impossible with single inheritance)
The base class is becoming a fragile dependency that breaks on every change
Migration steps:
Extract each abstract/virtual step into its own interface: IDataFetcher, IDataFormatter, etc.
Move each subclass's step implementation into a strategy class that implements the corresponding interface.
Change the base class from abstract to concrete. Replace inheritance with constructor injection of strategy interfaces.
The template method now calls _fetcher.Fetch() instead of FetchData() virtual method.
Wire up the strategies in DI. Old subclass combinations become DI registrations.
Great Answer Bonus "The migration is backward-compatible if you keep the old abstract class as a thin wrapper that creates strategies from its overrides. Old subclasses continue to work, new code uses the strategy-based approach. You deprecate the old class and remove it when all subclasses have migrated."
Q27: Template Method and thread safety — what are the concerns?
Hard
Think First What happens when two threads call the template method on the same instance?
Template Method itself doesn't introduce threading issues — it's just method calls. But the pattern creates conditions that make threading bugs more likely:
Shared mutable state in the base class — If the base class has fields that steps read and write, and two threads run the template concurrently, those fields become a race condition. Step A on thread 1 writes to _currentData, then step A on thread 2 overwrites it before step B on thread 1 reads it.
Step ordering assumptions — The template guarantees step order within one call, but not across concurrent calls. If step B depends on state set by step A, concurrent calls interleave the state.
Hook side effects — Hooks that modify shared state (counters, caches, audit logs) must be thread-safe.
Mitigations: Make the template method instance stateless — pass data between steps via return values and parameters, not fields. If state is unavoidable, use a per-call context object. For singleton template method instances in DI, ensure all step implementations are thread-safe.
Great Answer Bonus "The safest pattern is a stateless base class with a context object: var ctx = new PipelineContext(); Step1(ctx); Step2(ctx); Step3(ctx);. Each call gets its own context, so concurrent calls can't interfere with each other."
Q28: How does Template Method compare to the Chain of Responsibility?
Hard
Think First Both involve a sequence of operations. What's the key difference in how they execute?
Both patterns process work through a sequence, but they differ fundamentally in control flow:
Aspect
Template Method
Chain of Responsibility
Structure
Single class with overridable steps
Linked list of independent handler objects
Execution
All steps always run, in fixed order
Handlers can stop the chain (short-circuit)
Coupling
Steps tightly coupled via inheritance
Handlers loosely coupled via interface
Variation
Steps vary by subclass (compile-time)
Chain composition varies (runtime)
Control
Base class controls everything
Each handler decides whether to continue
Example
Report: fetch → format → save (all always run)
Exception handling: first matching handler wins
When to use which: Template Method when all steps must always execute in a fixed order. Chain of Responsibility when you need optional processing where any handler can handle the request and stop the chain.
Great Answer Bonus "ASP.NET Core middleware is an interesting hybrid. The pipeline has a fixed structure (like Template Method), but each middleware can short-circuit by not calling next() (like Chain of Responsibility). It borrows the best of both patterns."
Q29: Design a game loop using Template Method — explain your approach.
Hard
Think First A game loop always follows the same structure: process input, update state, render. What else does a production game loop need?
A game loop is one of the most natural fits for Template Method. Every game follows the same high-level loop: initialize → [process input → update game state → render] → cleanup. But the actual input handling, game logic, and rendering differ wildly between games.
The beauty: the game loop structure (init, loop, cleanup) is identical for every game. Only the per-game logic changes. Adding a new game means creating one class and filling in four methods. The framework handles timing, the loop, and shutdown.
Great Answer Bonus "Real game engines like Unity use this exact pattern. MonoBehaviour has Awake(), Start(), Update(), FixedUpdate(), OnDestroy() — all hooks in a template method that the engine calls at the right time. You never call Update() yourself; the engine does."
Section 18
Practice Exercises
Four exercises to cement your Template Method skills. Start with the Beverage Maker to get the basic shape down, then tackle progressively trickier scenarios. By the end, you'll be writing template methods with hooks and cancellation support — the kind of thing you'd see in a real production codebase.
Exercise 1: Build a Beverage Maker
Easy
Create a HotBeverage base class with a MakeBeverage()template methodA method in the base class that defines the sequence of steps. Subclasses override individual steps but can't change the order. that calls four steps in order: BoilWater(), Brew(), Pour(), and AddCondiments(). Boiling water and pouring are the same for every drink, so implement them in the base. Brewing and adding condiments differ — make those abstract. Then create Tea and Coffee subclasses.
Hints
The MakeBeverage() method should be sealed (or non-virtual) so subclasses can't change the order of steps
BoilWater() and Pour() are concrete methods in the base — same for tea and coffee
Brew() and AddCondiments() should be protected abstract — each drink provides its own version
Tea brews tea leaves and adds lemon; Coffee brews ground beans and adds milk + sugar
Use Console.WriteLine() for each step so you can see the order when running
Solution
HotBeverage.cs
public abstract class HotBeverage
{
// Template method β sealed so subclasses can't reorder steps
public sealed void MakeBeverage()
{
BoilWater();
Brew();
Pour();
AddCondiments();
}
// Shared steps β same for every beverage
private void BoilWater()
=> Console.WriteLine("Boiling water...");
private void Pour()
=> Console.WriteLine("Pouring into cup...");
// Abstract steps β each drink fills in its own details
protected abstract void Brew();
protected abstract void AddCondiments();
}
public class Tea : HotBeverage
{
protected override void Brew()
=> Console.WriteLine("Steeping the tea leaves for 3 minutes");
protected override void AddCondiments()
=> Console.WriteLine("Adding a slice of lemon");
}
public class Coffee : HotBeverage
{
protected override void Brew()
=> Console.WriteLine("Dripping coffee through filter");
protected override void AddCondiments()
=> Console.WriteLine("Adding milk and sugar");
}
// Usage
var tea = new Tea();
tea.MakeBeverage();
// Output:
// Boiling water...
// Steeping the tea leaves for 3 minutes
// Pouring into cup...
// Adding a slice of lemon
var coffee = new Coffee();
coffee.MakeBeverage();
// Output:
// Boiling water...
// Dripping coffee through filter
// Pouring into cup...
// Adding milk and sugar
Exercise 2: Document Generator
Medium
Build a DocumentGenerator base class with a Generate() template method that calls five steps: FetchData(), FormatHeader(), FormatBody(), FormatFooter(), and SaveOutput(). Fetching data is shared logic. Formatting steps are abstract — each output format has its own style. Saving is abstract too (PDF writes bytes, HTML writes a string). Implement PdfGenerator and HtmlGenerator.
Hints
FetchData() can be a concrete method that returns a Dictionary<string, object> or a simple model class
Store the fetched data as a protected field so subclass formatting steps can use it
The three Format methods should each return a string — the base class concatenates them
SaveOutput(string content) is abstract — PDF might write to a binary stream, HTML writes to a .html file
Consider making Generate() return Task if you want to practice async template methods
The template method should be sealed
Solution
DocumentGenerator.cs
public record ReportData(string Title, List<string> Rows, DateTime GeneratedAt);
public abstract class DocumentGenerator
{
protected ReportData Data { get; private set; } = default!;
// Template method β defines the pipeline
public sealed void Generate()
{
Data = FetchData();
var header = FormatHeader();
var body = FormatBody();
var footer = FormatFooter();
var content = $"{header}\n{body}\n{footer}";
SaveOutput(content);
}
// Shared step β same data source for all formats
private ReportData FetchData()
{
// In real code, this would hit a database or API
return new ReportData(
"Monthly Sales Report",
new List<string> { "Jan: $10K", "Feb: $12K", "Mar: $9K" },
DateTime.UtcNow);
}
// Abstract steps β each format provides its own layout
protected abstract string FormatHeader();
protected abstract string FormatBody();
protected abstract string FormatFooter();
protected abstract void SaveOutput(string content);
}
public class HtmlGenerator : DocumentGenerator
{
private readonly string _outputPath;
public HtmlGenerator(string outputPath) => _outputPath = outputPath;
protected override string FormatHeader()
=> $"<html><head><title>{Data.Title}</title></head><body><h1>{Data.Title}</h1>";
protected override string FormatBody()
{
var rows = string.Join("", Data.Rows.Select(r => $"<li>{r}</li>"));
return $"<ul>{rows}</ul>";
}
protected override string FormatFooter()
=> $"<footer>Generated: {Data.GeneratedAt:yyyy-MM-dd}</footer></body></html>";
protected override void SaveOutput(string content)
=> File.WriteAllText(_outputPath, content);
}
public class PdfGenerator : DocumentGenerator
{
private readonly string _outputPath;
public PdfGenerator(string outputPath) => _outputPath = outputPath;
protected override string FormatHeader()
=> $"=== {Data.Title.ToUpper()} ===\n";
protected override string FormatBody()
=> string.Join("\n", Data.Rows.Select(r => $" * {r}"));
protected override string FormatFooter()
=> $"\n--- Generated: {Data.GeneratedAt:yyyy-MM-dd} ---";
protected override void SaveOutput(string content)
{
// In production you'd use a PDF library (e.g. QuestPDF)
// For this exercise, we'll save as plain text
File.WriteAllText(_outputPath, content);
Console.WriteLine($"PDF saved to {_outputPath}");
}
}
Exercise 3: Game Character AI
Medium
Create an EnemyAI base class with a TakeTurn() template method that calls four steps: Assess() (evaluate the battlefield), Move() (reposition), Attack() (deal damage), and Retreat() (fall back if health is low). Implement AggressiveAI (charges at the player, attacks hard, rarely retreats) and CautiousAI (keeps distance, snipes, retreats early). Think about which steps should be abstract and which could be hooks.
Hints
Assess() could be abstract — aggressive AI assesses enemy proximity, cautious AI assesses escape routes
Move() and Attack() must be abstract — completely different strategies for each AI type
Retreat() is a great candidate for a hookAn optional step in the template. It has a default implementation (often empty or returning true) that subclasses CAN override but don't have to. Unlike abstract methods, hooks are voluntary. — give it a default (do nothing) since aggressive AI might never retreat
Consider adding a ShouldRetreat() hook that returns bool — the template method checks it before calling Retreat()
Pass a simple GameState record to the template method so steps can read current HP, enemy position, etc.
Solution
EnemyAI.cs
public record GameState(int MyHealth, int EnemyHealth, double DistanceToEnemy);
public abstract class EnemyAI
{
public string Name { get; }
protected EnemyAI(string name) => Name = name;
// Template method β the turn sequence is fixed
public sealed void TakeTurn(GameState state)
{
Console.WriteLine($"\n--- {Name}'s Turn ---");
Assess(state);
Move(state);
Attack(state);
// Hook: only retreat if the subclass says so
if (ShouldRetreat(state))
Retreat(state);
}
// Abstract steps β every AI must implement these
protected abstract void Assess(GameState state);
protected abstract void Move(GameState state);
protected abstract void Attack(GameState state);
// Hook β optional, default is "don't retreat"
protected virtual bool ShouldRetreat(GameState state) => false;
protected virtual void Retreat(GameState state) { }
}
public class AggressiveAI : EnemyAI
{
public AggressiveAI() : base("Berserker") { }
protected override void Assess(GameState state)
=> Console.WriteLine($" Targeting enemy at distance {state.DistanceToEnemy:F1}");
protected override void Move(GameState state)
=> Console.WriteLine(" Charging straight at the player!");
protected override void Attack(GameState state)
=> Console.WriteLine(" Heavy melee strike! (30 damage)");
// Never retreats β doesn't override ShouldRetreat, so the hook's
// default (false) keeps the berserker fighting until death
}
public class CautiousAI : EnemyAI
{
public CautiousAI() : base("Sniper") { }
protected override void Assess(GameState state)
=> Console.WriteLine($" Scanning for cover... HP: {state.MyHealth}");
protected override void Move(GameState state)
=> Console.WriteLine(" Moving behind cover, maintaining distance");
protected override void Attack(GameState state)
=> Console.WriteLine(" Ranged shot from cover (15 damage)");
// Retreat early when health drops below 30%
protected override bool ShouldRetreat(GameState state)
=> state.MyHealth < 30;
protected override void Retreat(GameState state)
=> Console.WriteLine(" Falling back to a safer position!");
}
// Usage
var state = new GameState(MyHealth: 25, EnemyHealth: 60, DistanceToEnemy: 8.5);
new AggressiveAI().TakeTurn(state);
// --- Berserker's Turn ---
// Targeting enemy at distance 8.5
// Charging straight at the player!
// Heavy melee strike! (30 damage)
new CautiousAI().TakeTurn(state);
// --- Sniper's Turn ---
// Scanning for cover... HP: 25
// Moving behind cover, maintaining distance
// Ranged shot from cover (15 damage)
// Falling back to a safer position!
Exercise 4: Data Pipeline with Hooks
Hard
Build a DataPipeline<T> base class with an Execute() template method that calls Validate(), Transform(), and Load(). Add two optional hooksVirtual methods with a default (often empty) implementation. Subclasses can override them if needed, but they're not forced to. Hooks let you add optional customization points without cluttering the contract with mandatory abstract methods.: OnBeforeTransform() and OnAfterLoad(). The pipeline should also accept a CancellationTokenA .NET mechanism for cooperative cancellation. You pass a token to async methods, and they periodically check if cancellation was requested. This lets you gracefully stop long-running operations without killing threads. so long-running steps can be cancelled. Implement a CsvToDbPipeline that validates CSV headers, transforms rows into entities, and bulk-inserts them into a database.
Hints
Make Execute(CancellationToken ct) return Task — all steps should be async
Validate(), Transform(), and Load() are protected abstract Task
Hooks are protected virtual Task with default empty implementation: return Task.CompletedTask;
Check ct.ThrowIfCancellationRequested() between each step
Wrap the whole pipeline in a try/catch — if any step fails, log the error and rethrow
The generic <T> represents the entity type being processed (e.g., DataPipeline<CustomerRecord>)
Store intermediate results in a protected List<T> Items property
Everything you need in one scannable grid. Bookmark this section — it's the fastest reference when you're implementing a template method and need a quick reminder of the rules.
Pattern Type
Category: Behavioral (GoFGang of Four — the four authors (Gamma, Helm, Johnson, Vlissides) who wrote the foundational book “Design Patterns” in 1994. When someone says “GoF pattern,” they mean one of the 23 patterns from that book.)
Purpose: Define the skeleton of
an algorithm in a base class,
letting subclasses fill in
specific steps without changing
the overall structure.
A.k.a: "The recipe pattern"
— the recipe is fixed,
the ingredients can change.
Intent
Define an algorithm's skeleton
in a base-class method.
Defer certain steps to subclasses.
Subclasses redefine specific
steps WITHOUT changing the
algorithm's structure.
Key word: "skeleton" — the
bones stay the same, the
flesh can change.
Key Players
AbstractClass
→ Defines TemplateMethod()
→ Declares abstract steps
→ May include hooks
ConcreteClass
→ Extends AbstractClass
→ Overrides abstract steps
→ Optionally overrides hooks
TemplateMethod()
→ sealed / non-virtual
→ Calls steps in fixed order
Sealed Rule
The template method itself
should ALWAYS be sealed (C#)
or non-virtual.
Why? If subclasses can override
the template method, they can
change the algorithm's structure
— defeating the whole point.
✓ public sealed void Process()
✗ public virtual void Process()
✗ public void Process() // no seal
Hook vs Abstract
Abstract Step:
→ MUST override (required)
→ No default behavior
→ protected abstract void X();
Hook Method:
→ CAN override (optional)
→ Has a default (often empty)
→ protected virtual void X()
=> { } // do nothing
Rule of thumb:
Different every time? Abstract.
Sometimes needed? Hook.
Hollywood Principle
"Don't call us, we'll call you."
Also called Inversion of ControlInstead of your code calling a library, the library/framework calls YOUR code. Template Method is a classic example: the base class controls the flow and calls your overridden steps at the right time..
The base class (Hollywood) calls
the subclass methods (actors)
at the right time.
Subclasses NEVER call the
template method — they just
fill in the blanks and wait
to be called.
This inverts the usual control:
framework calls your code,
not the other way around.
.NET Examples
BackgroundService
→ ExecuteAsync() is your step
→ Framework calls Start/Stop
TagHelper (ASP.NET)
→ Process() is your step
→ Razor engine calls it
Stream (System.IO)
→ Read/Write are steps
→ CopyTo() is the template
HttpMessageHandler
→ SendAsync() is your step
→ HttpClient calls it
ControllerBase (MVC)
→ OnActionExecuting() hook
→ Pipeline calls your action
Common Pitfall
✗ Forgetting to seal the
template method
→ Subclass overrides it
→ Algorithm breaks
✗ Too many abstract steps
→ Subclass becomes a chore
→ Keep it to 3-5 steps
✗ Calling abstract methods
in the constructor
→ Subclass isn't ready yet
→ NullReferenceException
✗ Mixing hooks and abstracts
without clear naming
→ Devs don't know which
to override
When NOT To Use
✗ Only one implementation
→ Just write a normal class
✗ Need to swap at runtime
→ Use Strategy instead
✗ Steps vary independently
→ Compose with Strategy
✗ No shared algorithm
→ If the sequence differs
between variants, Template
Method can't help
✗ Deep inheritance (>2 levels)
→ Fragile base class problem
→ Prefer composition
Section 20
Deep Dive: Hook Methods
Hooks are one of the most misunderstood parts of the Template Method pattern. They look innocent — just little virtual methods with empty bodies — but they're the difference between a rigid framework that forces everyone into the same mold and a flexible one that bends to each user's needs. Let's unpack them properly.
What Are Hooks?
Think of a recipe with optional steps. "Add chili flakes if you like it spicy." That "if you like" part is a hook. You can do it, but you don't have to. The recipe works either way.
In code, a hook methodA virtual method in the base class with a default implementation (usually empty). Subclasses CAN override it but aren't forced to. It's an optional customization point in the algorithm. is a virtual method in the base class with a default implementation — usually empty or returning a harmless default value. Subclasses can override it to plug in custom behavior, but they're not required to. Compare that with an abstract method, which forces every subclass to provide an implementation or the code won't compile.
Here's the key difference at a glance:
AbstractStep.cs
public abstract class ReportGenerator
{
public sealed void Generate()
{
FetchData();
FormatReport(); // MUST override β won't compile otherwise
SaveReport(); // MUST override
}
private void FetchData() => Console.WriteLine("Fetching data...");
// Abstract = required. Every subclass MUST provide these.
protected abstract void FormatReport();
protected abstract void SaveReport();
}
// Won't compile if you forget FormatReport() or SaveReport():
// Error CS0534: 'MyReport' does not implement 'ReportGenerator.FormatReport()'
HookStep.cs
public abstract class ReportGenerator
{
public sealed void Generate()
{
FetchData();
OnBeforeFormat(); // Hook β runs if overridden, does nothing otherwise
FormatReport();
OnAfterSave(); // Hook β same deal
SaveReport();
}
private void FetchData() => Console.WriteLine("Fetching data...");
protected abstract void FormatReport();
protected abstract void SaveReport();
// Hooks = optional. Default does nothing.
protected virtual void OnBeforeFormat() { }
protected virtual void OnAfterSave() { }
}
// This compiles just fine β hooks are NOT required:
public class SimpleReport : ReportGenerator
{
protected override void FormatReport() => /* ... */;
protected override void SaveReport() => /* ... */;
// OnBeforeFormat and OnAfterSave silently do nothing β and that's OK
}
What Actually Happens When Hooks Are Misused
The most common mistake is putting important logic in a hook and then being surprised when a subclass doesn't override it. If a step is essential for the algorithm to work correctly, it must be abstract — not a hook. Hooks are for "nice to have" customizations: logging, notifications, validation shortcuts, progress reporting. If skipping the step would produce wrong results, don't make it a hook.
Designing Good Hooks
Good hooks follow a naming convention that tells developers exactly when they fire. The most common pattern is OnBefore___ and OnAfter___ — names that read like plain English and leave zero ambiguity about timing.
Hook Type
When Called
Default Behavior
Override Example
OnBeforeExecute()
Right before the main algorithm starts
Empty (does nothing)
Start a timer, open a transaction
OnAfterExecute()
Right after the main algorithm completes
Empty
Log duration, commit transaction
ShouldContinue()
Between steps, to check if we should proceed
Returns true
Return false to short-circuit
OnError(Exception)
When a step throws an exception
Empty (exception propagates)
Log error, send alert, clean up
OnStepCompleted(string)
After each individual step finishes
Empty
Update a progress bar
Three rules for hook design:
Keep hooks focused — one hook, one purpose. Don't create a single OnCustomize() mega-hook that tries to do everything
Document when they fire — add an XML comment explaining the exact moment in the algorithm when this hook is called. Future developers will thank you
Don't make too many — if your base class has 8+ hooks, it's probably doing too much. Three to five hooks is a healthy range
WellDesignedHooks.cs
public abstract class OrderProcessor
{
public sealed async Task ProcessAsync(Order order, CancellationToken ct)
{
await OnBeforeProcessAsync(order, ct); // Hook: setup, logging
await ValidateAsync(order, ct); // Abstract: required
await ChargePaymentAsync(order, ct); // Abstract: required
if (ShouldSendConfirmation(order)) // Hook: gate check
await SendConfirmationAsync(order, ct);
await OnAfterProcessAsync(order, ct); // Hook: cleanup, metrics
}
// Required steps
protected abstract Task ValidateAsync(Order order, CancellationToken ct);
protected abstract Task ChargePaymentAsync(Order order, CancellationToken ct);
protected abstract Task SendConfirmationAsync(Order order, CancellationToken ct);
/// <summary>Called before any processing begins. Use for logging or setup.</summary>
protected virtual Task OnBeforeProcessAsync(Order order, CancellationToken ct)
=> Task.CompletedTask;
/// <summary>Called after all processing completes. Use for metrics or cleanup.</summary>
protected virtual Task OnAfterProcessAsync(Order order, CancellationToken ct)
=> Task.CompletedTask;
/// <summary>Return false to skip the confirmation email. Default: true.</summary>
protected virtual bool ShouldSendConfirmation(Order order) => true;
}
Hooks vs Events
This question trips up even experienced developers: "Should I use a hook method or fire an event?" They both let external code react to something happening, but they serve different audiences and have different trade-offs.
Hooks are for subclass customization. They're part of the class's internal extension mechanism. Only someone who inherits from your base class can use them. It's an "inside the family" thing.
Events (or delegates in C#) are for external notification. Any code that holds a reference to your object can subscribe. It's an "open to the public" thing. You don't need to subclass anything — just attach a handler.
Aspect
Hook (virtual method)
Event / Delegate
Who uses it?
Subclasses only
Any external code
Coupling
Tight (inheritance)
Loose (subscription)
Multiple listeners?
No (one override per class)
Yes (many subscribers)
Can modify behavior?
Yes (change algorithm flow)
Usually no (notification only)
Discoverability
IDE shows overridable methods
Must read docs for event names
Best for
Framework extension points
Cross-cutting notifications
Key insight: If you need to change how the algorithm works, use a hook. If you need to tell the outside world something happened, use an event. Many production systems use both — hooks for subclass customization, eventsIn C#, events are a language feature built on delegates. Any code with a reference to the object can subscribe (+=) to an event and get notified when it fires. Unlike hooks, events support multiple subscribers and don't require inheritance. for external subscribers.
HookPlusEvent.cs
public abstract class FileProcessor
{
// Event β external code subscribes to get notified
public event EventHandler<string>? FileProcessed;
public sealed void Process(string path)
{
OnBeforeProcess(path); // Hook β subclass customization
var result = DoProcess(path); // Abstract step
OnAfterProcess(result); // Hook β subclass customization
FileProcessed?.Invoke(this, path); // Event β external notification
}
protected abstract string DoProcess(string path);
// Hooks (for subclasses)
protected virtual void OnBeforeProcess(string path) { }
protected virtual void OnAfterProcess(string result) { }
}
// Subclass uses the HOOK to customize behavior:
public class CsvProcessor : FileProcessor
{
protected override string DoProcess(string path) => /* parse CSV */;
protected override void OnBeforeProcess(string path)
=> Console.WriteLine($"Validating CSV headers in {path}");
}
// External code uses the EVENT to react:
var processor = new CsvProcessor();
processor.FileProcessed += (sender, path) =>
Console.WriteLine($"Dashboard: {path} was processed!"); // notification
Section 21
Real-World Mini-Project: Data Import Pipeline
Production note: Real .NET apps use IDbConnection (DapperA lightweight ORM (Object-Relational Mapper) for .NET. It maps SQL query results directly to C# objects with minimal overhead. Much faster than Entity Framework for raw SQL queries.), CsvHelper for CSV parsing, and System.Text.Json for JSON deserialization. This mini-project deliberately builds the import pipeline from scratch to show how Template Method thinking applies — it's a learning exercise, not a replacement for production libraries.
We'll build the same data import system three times. Each attempt fixes the problems of the one before it. By Attempt 3, you'll have a production-ready pipeline that uses the Template Method pattern to cleanly handle CSV, JSON, and XML imports through a single shared algorithm.
Attempt 1: The Naive Approach
The first instinct is to handle all import formats in one method with a big if/else chain. Each branch does the same thing — read, parse, validate, save — but with slightly different details for each format. It works today, but it's a ticking time bomb.
NaiveImporter.cs
public class DataImporter
{
public void ImportData(string filePath, string format)
{
if (format == "csv")
{
var lines = File.ReadAllLines(filePath);
var headers = lines[0].Split(',');
// Validate CSV headers
if (!headers.Contains("Id") || !headers.Contains("Name"))
throw new Exception("Invalid CSV headers");
foreach (var line in lines.Skip(1))
{
var cols = line.Split(',');
var record = new { Id = cols[0], Name = cols[1] };
// Save to database...
Console.WriteLine($"CSV: Imported {record.Name}");
}
}
else if (format == "json")
{
var json = File.ReadAllText(filePath);
var items = JsonSerializer.Deserialize<List<Dictionary<string, string>>>(json);
// Validate JSON structure
if (items == null || items.Count == 0)
throw new Exception("Empty JSON array");
foreach (var item in items)
{
// Save to database...
Console.WriteLine($"JSON: Imported {item["Name"]}");
}
}
else if (format == "xml")
{
var doc = XDocument.Load(filePath);
var records = doc.Descendants("Record");
// Validate XML
if (!records.Any())
throw new Exception("No records found in XML");
foreach (var record in records)
{
var name = record.Element("Name")?.Value;
// Save to database...
Console.WriteLine($"XML: Imported {name}");
}
}
else
{
throw new NotSupportedException($"Unknown format: {format}");
}
}
}
3 Critical Bugs
No error handling — If the CSV has a malformed row, the whole import crashes halfway through. Some records are saved, others aren't. No rollback, no cleanup.
No cleanup on failure — If the database insert fails on row 50 of 100, the first 49 rows are orphaned. There's no transaction wrapping, no way to undo the damage.
Adding a format requires modifying existing code — Want to support Excel? You have to crack open this method and add yet another else if. Every change risks breaking CSV, JSON, and XML imports that already work.
Attempt 2: Separate Methods, Same Class
The second attempt extracts each format into its own method. Cleaner, but they're still in the same class. Every time you add a format, you modify this class — and everyone's code depends on it.
Still violates OCP — Adding Excel means modifying ReadFile(), ParseData(), and adding a new ParseExcel() method. Three changes to an existing class.
Hard to test individual steps — You can't test CSV parsing in isolation because it's a private method buried inside a class that also handles JSON and XML.
One giant class with growing switch statements — Every format switch gets a new case. At 10 formats this class is a mess.
Attempt 3: Production-Ready Template Method
Now we apply the Template Method pattern properly. The shared algorithm — read, validate, parse, save — lives in an abstract base class. Each format becomes its own subclass that only overrides the steps that differ. Adding a new format means creating one new class with zero changes to existing code — the Open/Closed PrincipleSoftware should be open for extension (add new behavior via new classes) but closed for modification (don't change code that already works). Template Method achieves this: new formats = new subclasses, not edits to the base. in action.
DataImporter.cs
public record ImportRecord(string Id, string Name, Dictionary<string, string> Data);
/// <summary>
/// Template Method base class for data import pipelines.
/// The algorithm is fixed: Read β Validate β Parse β Save.
/// Subclasses provide format-specific logic for each step.
/// </summary>
public abstract class DataImporter
{
protected ILogger Logger { get; }
protected List<ImportRecord> Records { get; set; } = new();
protected DataImporter(ILogger logger) => Logger = logger;
// Template method β sealed to lock the algorithm
public sealed async Task ImportAsync(string filePath, CancellationToken ct = default)
{
var sw = Stopwatch.StartNew();
Logger.LogInformation("Starting {Format} import from {Path}",
FormatName, filePath);
try
{
var rawData = await ReadFileAsync(filePath, ct);
ct.ThrowIfCancellationRequested();
ValidateRawData(rawData);
ct.ThrowIfCancellationRequested();
Records = ParseRecords(rawData);
ct.ThrowIfCancellationRequested();
await OnBeforeSaveAsync(Records, ct); // Hook
await SaveRecordsAsync(Records, ct);
await OnAfterSaveAsync(Records, ct); // Hook
Logger.LogInformation(
"{Format} import complete: {Count} records in {Elapsed}ms",
FormatName, Records.Count, sw.ElapsedMilliseconds);
}
catch (OperationCanceledException)
{
Logger.LogWarning("{Format} import cancelled", FormatName);
throw;
}
catch (Exception ex)
{
Logger.LogError(ex, "{Format} import failed", FormatName);
throw;
}
}
// Identity β each format provides its name
protected abstract string FormatName { get; }
// Required steps β subclasses MUST implement
protected abstract Task<string> ReadFileAsync(string path, CancellationToken ct);
protected abstract void ValidateRawData(string rawData);
protected abstract List<ImportRecord> ParseRecords(string rawData);
protected abstract Task SaveRecordsAsync(List<ImportRecord> records, CancellationToken ct);
// Hooks β optional customization points
protected virtual Task OnBeforeSaveAsync(List<ImportRecord> records, CancellationToken ct)
=> Task.CompletedTask;
protected virtual Task OnAfterSaveAsync(List<ImportRecord> records, CancellationToken ct)
=> Task.CompletedTask;
}
CsvImporter.cs
public class CsvImporter : DataImporter
{
private readonly IDbConnection _db;
public CsvImporter(IDbConnection db, ILogger<CsvImporter> logger)
: base(logger)
{
_db = db;
}
protected override string FormatName => "CSV";
protected override async Task<string> ReadFileAsync(string path, CancellationToken ct)
=> await File.ReadAllTextAsync(path, ct);
protected override void ValidateRawData(string rawData)
{
var firstLine = rawData.Split('\n')[0];
var headers = firstLine.Split(',').Select(h => h.Trim()).ToArray();
if (!headers.Contains("Id") || !headers.Contains("Name"))
throw new InvalidDataException(
$"CSV missing required columns. Found: {string.Join(", ", headers)}");
}
protected override List<ImportRecord> ParseRecords(string rawData)
{
var lines = rawData.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var headers = lines[0].Split(',').Select(h => h.Trim()).ToArray();
return lines.Skip(1).Select(line =>
{
var cols = line.Split(',');
var data = headers.Zip(cols, (h, c) => (h, c))
.ToDictionary(x => x.h, x => x.c.Trim());
return new ImportRecord(data["Id"], data["Name"], data);
}).ToList();
}
protected override async Task SaveRecordsAsync(
List<ImportRecord> records, CancellationToken ct)
{
const string sql = "INSERT INTO Imports (Id, Name, RawData) " +
"VALUES (@Id, @Name, @Data)";
foreach (var batch in records.Chunk(100))
{
ct.ThrowIfCancellationRequested();
await _db.ExecuteAsync(sql, batch);
}
}
// Override hook: deduplicate before saving
protected override Task OnBeforeSaveAsync(
List<ImportRecord> records, CancellationToken ct)
{
var before = records.Count;
Records = records.DistinctBy(r => r.Id).ToList();
if (Records.Count < before)
Logger.LogWarning("Removed {Count} duplicate CSV rows",
before - Records.Count);
return Task.CompletedTask;
}
}
JsonImporter.cs
public class JsonImporter : DataImporter
{
private readonly IDbConnection _db;
public JsonImporter(IDbConnection db, ILogger<JsonImporter> logger)
: base(logger)
{
_db = db;
}
protected override string FormatName => "JSON";
protected override async Task<string> ReadFileAsync(string path, CancellationToken ct)
=> await File.ReadAllTextAsync(path, ct);
protected override void ValidateRawData(string rawData)
{
try
{
using var doc = JsonDocument.Parse(rawData);
if (doc.RootElement.ValueKind != JsonValueKind.Array)
throw new InvalidDataException("JSON root must be an array");
if (doc.RootElement.GetArrayLength() == 0)
throw new InvalidDataException("JSON array is empty");
}
catch (JsonException ex)
{
throw new InvalidDataException($"Invalid JSON: {ex.Message}");
}
}
protected override List<ImportRecord> ParseRecords(string rawData)
{
using var doc = JsonDocument.Parse(rawData);
return doc.RootElement.EnumerateArray().Select(el =>
{
var dict = new Dictionary<string, string>();
foreach (var prop in el.EnumerateObject())
dict[prop.Name] = prop.Value.ToString();
return new ImportRecord(dict["Id"], dict["Name"], dict);
}).ToList();
}
protected override async Task SaveRecordsAsync(
List<ImportRecord> records, CancellationToken ct)
{
const string sql = "INSERT INTO Imports (Id, Name, RawData) " +
"VALUES (@Id, @Name, @Data)";
foreach (var batch in records.Chunk(100))
{
ct.ThrowIfCancellationRequested();
await _db.ExecuteAsync(sql, batch);
}
}
}
Program.cs
// DI registration β each format gets its own class
builder.Services.AddScoped<IDbConnection>(sp =>
new SqlConnection(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<CsvImporter>();
builder.Services.AddScoped<JsonImporter>();
// Optionally, register a factory for format selection:
builder.Services.AddScoped<Func<string, DataImporter>>(sp => format =>
format.ToLower() switch
{
"csv" => sp.GetRequiredService<CsvImporter>(),
"json" => sp.GetRequiredService<JsonImporter>(),
_ => throw new NotSupportedException($"Format: {format}")
});
// Usage in a controller or background service:
app.MapPost("/import", async (
IFormFile file,
string format,
Func<string, DataImporter> importerFactory,
CancellationToken ct) =>
{
var tempPath = Path.GetTempFileName();
await using (var stream = File.Create(tempPath))
await file.CopyToAsync(stream, ct);
var importer = importerFactory(format);
await importer.ImportAsync(tempPath, ct);
return Results.Ok(new { message = $"Imported via {format}" });
});
Why This Is Production-Ready
Extensible
Adding a new format (Excel, Parquet, etc.) means creating one new class that extends DataImporter. Zero changes to CSV, JSON, or the base class. True Open/Closed PrincipleA class should be open for extension (add new behavior via subclasses) but closed for modification (don't change existing code that already works)..
Testable
Each importer is a standalone class. Test CsvImporter.ParseRecords() with a mock string — no JSON or XML code anywhere in the test. Isolated, fast, reliable.
Error Handling
The base class wraps everything in try/catch with structured logging. Cancellation tokens stop long imports gracefully. Each step can fail independently without corrupting data.
Clean Separation
The algorithm (read → validate → parse → save) lives in one place. Format-specific logic lives in subclasses. Hooks let individual formats add extras (deduplication, notifications) without affecting the shared flow.
Section 22
Migration Guide: if/else → Template Method
You've got a method full of if/else or switch blocks, and every new variant means cracking open that same method and adding another branch. Here's how to refactor it into a clean Template Method design, step by step, without breaking anything that already works.
Step 1: Identify the Repeated Structure
Before you refactor anything, look for the pattern. You need methods that do the same thing in the same order but with different details. If the structure is the same — validate, process, save — but the specifics change (CSV validation vs JSON validation), that's your template method candidate.
SpotThePattern.cs
// Three methods with the SAME structure, DIFFERENT details:
public void ExportAsPdf(Report report)
{
var data = FetchReportData(report.Id); // Step 1: same
var header = FormatPdfHeader(data); // Step 2: different
var body = FormatPdfBody(data); // Step 3: different
SaveAsPdf(header + body, report.Path); // Step 4: different
}
public void ExportAsHtml(Report report)
{
var data = FetchReportData(report.Id); // Step 1: same
var header = FormatHtmlHeader(data); // Step 2: different
var body = FormatHtmlBody(data); // Step 3: different
SaveAsHtml(header + body, report.Path); // Step 4: different
}
public void ExportAsCsv(Report report)
{
var data = FetchReportData(report.Id); // Step 1: same
var header = FormatCsvHeader(data); // Step 2: different
var body = FormatCsvBody(data); // Step 3: different
SaveAsCsv(header + body, report.Path); // Step 4: different
}
// Pattern: Fetch β Format Header β Format Body β Save
// Steps 2, 3, 4 vary by format. Step 1 is identical.
// This is EXACTLY what Template Method was designed for.
Risk: Low. You're just reading code and drawing lines. No changes yet.
Step 2: Extract the Base Class
Create an abstract class that contains the shared algorithm. The steps that are the same go in as concrete methods. The steps that differ become abstract methods — placeholders that subclasses will fill in.
ReportExporter.cs
public abstract class ReportExporter
{
// Template method β the shared algorithm
public sealed void Export(Report report)
{
var data = FetchReportData(report.Id); // Shared (concrete)
var header = FormatHeader(data); // Varies (abstract)
var body = FormatBody(data); // Varies (abstract)
SaveOutput(header + body, report.Path); // Varies (abstract)
}
// Shared step β same for all formats
private ReportData FetchReportData(int id)
{
// Same database query regardless of export format
return _repository.GetReportData(id);
}
// Abstract steps β each format provides its own version
protected abstract string FormatHeader(ReportData data);
protected abstract string FormatBody(ReportData data);
protected abstract void SaveOutput(string content, string path);
}
Risk: Medium. You're creating new code, but you haven't deleted anything yet. The old if/else code still works. Run your existing tests to make sure nothing broke.
Step 3: Create Concrete Classes
Now move each format's logic into its own subclass. One class per variant — PdfExporter, HtmlExporter, CsvExporter. Each one overrides the abstract steps with the format-specific code that was inside those if/else branches.
ConcreteExporters.cs
public class PdfExporter : ReportExporter
{
protected override string FormatHeader(ReportData data)
=> $"=== {data.Title.ToUpper()} ===\n";
protected override string FormatBody(ReportData data)
=> string.Join("\n", data.Rows.Select(r => $" * {r}"));
protected override void SaveOutput(string content, string path)
{
// Use a PDF library like QuestPDF in production
File.WriteAllText(path, content);
}
}
public class HtmlExporter : ReportExporter
{
protected override string FormatHeader(ReportData data)
=> $"<h1>{data.Title}</h1>";
protected override string FormatBody(ReportData data)
{
var rows = string.Join("", data.Rows.Select(r => $"<li>{r}</li>"));
return $"<ul>{rows}</ul>";
}
protected override void SaveOutput(string content, string path)
=> File.WriteAllText(path, $"<html><body>{content}</body></html>");
}
public class CsvExporter : ReportExporter
{
protected override string FormatHeader(ReportData data)
=> "Id,Name,Value\n";
protected override string FormatBody(ReportData data)
=> string.Join("\n", data.Rows.Select(r => $"{r.Id},{r.Name},{r.Value}"));
protected override void SaveOutput(string content, string path)
=> File.WriteAllText(path, content);
}
Risk: Low. You're creating new classes that implement the same logic that was in the if/else branches. Test each one individually against the expected output.
Step 4: Wire Up DI & Test
Register your new classes in the dependency injectionA technique where objects receive their dependencies from outside (usually a DI container) instead of creating them internally. In .NET, this means registering services in Program.cs and letting the framework provide them via constructor parameters. container, replace the old if/else call sites, and run the full test suite to verify everything still works.
Program.cs
// Register each exporter
builder.Services.AddScoped<PdfExporter>();
builder.Services.AddScoped<HtmlExporter>();
builder.Services.AddScoped<CsvExporter>();
// Factory for format selection
builder.Services.AddScoped<Func<string, ReportExporter>>(sp => format =>
format.ToLower() switch
{
"pdf" => sp.GetRequiredService<PdfExporter>(),
"html" => sp.GetRequiredService<HtmlExporter>(),
"csv" => sp.GetRequiredService<CsvExporter>(),
_ => throw new NotSupportedException(format)
});
// Usage β the caller doesn't care which format
app.MapPost("/export", (
ExportRequest request,
Func<string, ReportExporter> exporterFactory) =>
{
var exporter = exporterFactory(request.Format);
exporter.Export(request.Report); // Polymorphism does the work
return Results.Ok();
});
Done! You've gone from a single method with growing if/else branches to a clean hierarchy where each format is its own self-contained class. Adding a new format now means creating one file — no changes to existing code.
Risk: Low. You're swapping call sites one at a time. Run tests after each swap. If anything breaks, revert just that one call site — the old code is still there until you're confident.
Section 23
Code Review Checklist
Use this checklist during code reviews to catch Template Method issues before they reach production. Each check targets a real mistake that causes real bugs — these aren't theoretical concerns.
#
Check
Why It Matters
Red Flag
1
Template method is sealed
If subclasses can override the template method itself, they can reorder or skip steps — defeating the entire point of the pattern
public virtual void Process() on the base class instead of public sealed void Process()
2
Abstract vs hook usage is correct
Making an essential step a hook means subclasses can accidentally skip it. Making an optional step abstract forces unnecessary boilerplate overrides
A hook that throws NotImplementedException in its default body (should be abstract), or an abstract method that most subclasses implement identically (should be hook/concrete)
3
Base constructor doesn't call abstract methods
In C#, the base constructorA special method that runs when an object is created. In C#, base class constructors run BEFORE subclass constructors — which means the subclass fields aren't set yet when the base constructor executes. runs before the subclass constructor. Calling an abstract method during construction hits a partially initialized objectWhen the base class constructor runs, the subclass's fields haven't been set yet. If the base calls an abstract method that the subclass overrides, that override runs with null/default fields. This is a common source of NullReferenceException.
Abstract method call inside the base class constructor body
4
Async steps return Task, not void
async voidAn async method that returns void instead of Task. The problem: if it throws an exception, there's no Task to observe it, so the exception vanishes silently (or crashes the app). Always return Task from async methods. swallows exceptions silently. If a step fails, the template method never finds out and continues with corrupted state
Inconsistent names confuse developers. If some hooks use OnBefore and others use Pre or BeforeDoing, nobody knows which convention to follow
Mix of OnBeforeProcess, PreValidation, and BeforeSaving in the same base class
6
Steps are protected, not public
Individual steps should only be called by the template method. Making them public lets callers bypass the algorithm and call steps out of order
public abstract void Validate() that external code calls directly, skipping the template method
7
No more than 5-7 abstract steps
Too many abstract steps means every new subclass is a chore. Developers will copy-paste implementations instead of thinking about each step
A base class with 10+ abstract methods that every subclass must implement
8
Hooks have documented calling order
Developers need to know exactly when each hook fires relative to the main steps. Without docs, they'll guess wrong and introduce bugs
Multiple hooks with no XML comments explaining when they're called in the algorithm sequence
9
No base.Method() call in sealed template
If a subclass somehow overrides the template method and calls base.Process(), the algorithm runs twice. The sealed keyword prevents this, but verify it's actually there
Template method that is virtual instead of sealed, with subclasses calling base.Process()
10
CancellationToken flows through all steps
If only some steps check the token, cancelling a long operation waits for non-token-aware steps to finish before actually stopping
Template method accepts CancellationToken but only passes it to 2 of 5 steps
11
Inheritance depth ≤ 2 levels
Deep hierarchies (Base → Mid → Concrete) create the fragile base class problemWhen changes to a base class unexpectedly break subclasses because of hidden dependencies between layers. The deeper the hierarchy, the more fragile it becomes. — changing the middle layer risks breaking all concrete classes
Three or more levels of abstract classes before reaching a concrete implementation
12
Unit tests exist for the template method flow
You need to verify that steps are called in the correct order and that hooks fire at the right time. A test subclass that records call order is the standard approach
Tests only for individual steps but no test that verifies the full algorithm sequence
Automate it with Roslyn Analyzers. Several of these checks can be enforced at compile time:
CA1033 — Interface methods should be callable by child types (catches access issues with protected steps)
CA1062 — Validate arguments of public methods (catches missing null checks in template method parameters)
CA1816 — Call GC.SuppressFinalize correctly (applies when your base class manages disposable resources)
CA2214 — Do not call overridable methods in constructors (catches check #3: abstract calls during construction)
Custom analyzer — Detect un-sealed template methods. Write a Roslyn analyzer that warns when a method named with the Template Method convention (e.g., Process, Execute, Run) calls abstract methods but isn't marked sealed.
Extract the shared algorithm into a base class. Each format becomes a subclass that only overrides the steps that differ. The template method in the base class calls the steps in order — subclasses can't change the flow, only the details.
Senior Solution: Rigid Format Logic
Each concrete class encapsulates its own formatting logic. Adding a new format means creating one new class — zero changes to existing code. This is OCP in action.
Senior Solution: Untestable
With Template Method, you test each concrete class independently. Mock the data source, call the template method, and verify the output. Each step can be tested in isolation by creating a test subclass that overrides just that step.