The template method pattern defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the overall structure. That sounds straightforward enough, but inheritance-based designs can quickly become brittle -- fragile base classes, sprawling abstract method lists, and subclasses that quietly break the algorithm's intended order. Template method pattern best practices in C# draw a clear line between maintainable implementations and ones that collapse under their own weight.
This guide covers the template method pattern best practices that keep hierarchies clean and testable. You'll work through sealing template methods, minimizing abstract method counts, designing effective hooks, keeping base classes focused, testing with spy subclasses, and integrating with dependency injection. Each section includes C# code examples comparing strong and weak approaches so you can evaluate tradeoffs for your own codebase.
Seal the Template Method
The template method itself -- the method that defines the algorithm's step order -- should not be overridable by subclasses. If a subclass can override it, the entire point of the pattern disappears. The base class no longer controls the algorithm's flow, and bugs from misordered steps become hard to trace.
In C#, mark the template method as non-virtual in the base class. If your base class inherits from another class that made the method virtual, use sealed override to lock it down:
public abstract class DataExporter
{
// Template method: non-virtual to protect the algorithm
public void Export(DataSet data)
{
Validate(data);
Transform(data);
WriteOutput(data);
}
protected abstract void Validate(DataSet data);
protected abstract void Transform(DataSet data);
protected abstract void WriteOutput(DataSet data);
}
Because Export is not marked virtual, no subclass can override it. The step order -- validate, transform, write -- is locked in. Subclasses control what happens inside each step, not when each step runs.
When your base class inherits from a parent that already declared the method as virtual, use sealed override:
public abstract class SpecializedExporter : DataExporter
{
public sealed override void Export(DataSet data)
{
OnBeforeExport(data);
base.Export(data);
OnAfterExport(data);
}
protected virtual void OnBeforeExport(DataSet data) { }
protected virtual void OnAfterExport(DataSet data) { }
}
The sealed keyword prevents any further subclass from redefining the enriched algorithm. This is a small constraint that prevents a large category of bugs. When a developer on your team creates CsvExporter : SpecializedExporter, they can implement the abstract steps and optionally hook into OnBeforeExport or OnAfterExport -- but they cannot rearrange the sequence.
Minimize Abstract Methods
Every abstract method in the template method pattern base class is a contract that every subclass must fulfill. When that list grows beyond three to five methods, subclasses become forced to implement steps they might not care about. The result is a proliferation of empty method bodies, throw new NotSupportedException() calls, and subclasses that feel like they're working against the pattern rather than with it.
Here's a base class with too many extension points:
// Avoid: too many abstract methods
public abstract class ReportGenerator
{
public void Generate(ReportRequest request)
{
Authenticate(request);
Authorize(request);
LoadData(request);
ValidateData(request);
ApplyFilters(request);
SortResults(request);
FormatHeaders(request);
FormatBody(request);
FormatFooters(request);
RenderOutput(request);
}
protected abstract void Authenticate(ReportRequest request);
protected abstract void Authorize(ReportRequest request);
protected abstract void LoadData(ReportRequest request);
protected abstract void ValidateData(ReportRequest request);
protected abstract void ApplyFilters(ReportRequest request);
protected abstract void SortResults(ReportRequest request);
protected abstract void FormatHeaders(ReportRequest request);
protected abstract void FormatBody(ReportRequest request);
protected abstract void FormatFooters(ReportRequest request);
protected abstract void RenderOutput(ReportRequest request);
}
Ten abstract methods means every concrete subclass must implement ten methods -- even if authentication and authorization are identical across all report types. This defeats the template method pattern's purpose of sharing common logic.
Refactor by keeping only the genuinely varying steps abstract, and push shared behavior into the base class or into hooks:
// Prefer: focused abstract methods with shared logic in the base
public abstract class ReportGenerator
{
private readonly IAuthService _authService;
protected ReportGenerator(IAuthService authService)
{
_authService = authService
?? throw new ArgumentNullException(nameof(authService));
}
public void Generate(ReportRequest request)
{
_authService.AuthenticateAndAuthorize(request);
var data = LoadData(request);
var formatted = FormatReport(data);
RenderOutput(formatted);
}
protected abstract ReportData LoadData(ReportRequest request);
protected abstract FormattedReport FormatReport(ReportData data);
protected abstract void RenderOutput(FormattedReport report);
}
Three abstract methods. Authentication and authorization are delegated to an injected service -- they don't vary by report type, so they don't belong as abstract steps. Each subclass now implements only the steps that genuinely differ: loading data from different sources, formatting in different styles, and rendering to different outputs. Keeping abstract methods minimal is one of the most impactful template method pattern best practices in C# because it directly reduces the burden on every subclass in the hierarchy. This aligns with how inversion of control pushes shared concerns into dependencies rather than inheritance hierarchies.
Design Effective Hooks
Hooks are virtual methods with default (often empty) implementations that give subclasses optional extension points. They differ from abstract methods because subclasses aren't forced to implement them. Well-designed hooks let subclasses participate in the algorithm at specific moments without touching the core flow.
Three guidelines keep hooks useful rather than confusing. First, name hooks with a clear convention that communicates when they fire -- OnBefore, OnAfter, and OnError prefixes eliminate ambiguity. Second, keep hook parameters and return types simple so subclasses don't need to understand internal state. Third, document exactly where in the algorithm the hook fires.
Here's a template method with well-designed hooks:
public abstract class OrderProcessor
{
public void ProcessOrder(Order order)
{
OnBeforeValidation(order);
Validate(order);
OnAfterValidation(order);
OnBeforePayment(order);
ProcessPayment(order);
OnAfterPayment(order);
OnBeforeFulfillment(order);
FulfillOrder(order);
OnAfterFulfillment(order);
}
protected abstract void Validate(Order order);
protected abstract void ProcessPayment(Order order);
protected abstract void FulfillOrder(Order order);
/// <summary>
/// Called before order validation begins.
/// Override to add logging or pre-validation enrichment.
/// </summary>
protected virtual void OnBeforeValidation(Order order) { }
/// <summary>
/// Called after validation completes successfully.
/// Override to trigger post-validation side effects.
/// </summary>
protected virtual void OnAfterValidation(Order order) { }
/// <summary>
/// Called before payment processing begins.
/// Override to add fraud checks or payment logging.
/// </summary>
protected virtual void OnBeforePayment(Order order) { }
/// <summary>
/// Called after payment completes successfully.
/// Override to send payment confirmation notifications.
/// </summary>
protected virtual void OnAfterPayment(Order order) { }
/// <summary>
/// Called before fulfillment begins.
/// Override to reserve inventory or notify warehouses.
/// </summary>
protected virtual void OnBeforeFulfillment(Order order) { }
/// <summary>
/// Called after fulfillment completes.
/// Override to send shipping confirmations.
/// </summary>
protected virtual void OnAfterFulfillment(Order order) { }
}
A subclass can override only the hooks it needs:
public sealed class AuditedOrderProcessor : OrderProcessor
{
private readonly IAuditLog _auditLog;
public AuditedOrderProcessor(IAuditLog auditLog)
{
_auditLog = auditLog
?? throw new ArgumentNullException(nameof(auditLog));
}
protected override void Validate(Order order)
{
// validation logic
}
protected override void ProcessPayment(Order order)
{
// payment logic
}
protected override void FulfillOrder(Order order)
{
// fulfillment logic
}
protected override void OnAfterPayment(Order order)
{
_auditLog.Record($"Payment processed for order {order.Id}");
}
protected override void OnAfterFulfillment(Order order)
{
_auditLog.Record($"Order {order.Id} fulfilled");
}
}
The AuditedOrderProcessor adds auditing at two specific points without touching the algorithm structure. It ignores hooks it doesn't need. Compare this approach to the decorator pattern, which wraps behavior externally -- hooks let you extend from within the inheritance hierarchy when that's the right tradeoff for your design.
Keep the Base Class Focused
The template method pattern base class should own the algorithm skeleton and nothing else. When teams pile utility methods, configuration handling, logging infrastructure, or data access into the base class, it becomes a "god base class" that every subclass inherits whether it needs those capabilities or not.
Here's a base class that has taken on too many responsibilities:
// Avoid: god base class
public abstract class FileImporter
{
protected ILogger Logger { get; }
protected IConfiguration Config { get; }
protected IDbConnection Connection { get; }
protected FileImporter(
ILogger logger,
IConfiguration config,
IDbConnection connection)
{
Logger = logger;
Config = config;
Connection = connection;
}
public void Import(string filePath)
{
Logger.LogInformation("Starting import of {File}", filePath);
var data = ReadFile(filePath);
var transformed = TransformData(data);
SaveToDatabase(transformed);
Logger.LogInformation("Import complete for {File}", filePath);
}
protected abstract RawData ReadFile(string filePath);
protected abstract ProcessedData TransformData(RawData data);
protected void SaveToDatabase(ProcessedData data)
{
var batchSize = Config.GetValue<int>("ImportBatchSize");
// ... database batch insert logic using Connection
}
protected string GetConfigValue(string key)
{
return Config[key]
?? throw new InvalidOperationException(
$"Missing config: {key}");
}
}
This base class handles logging, configuration access, database persistence, and the algorithm skeleton. Every subclass drags along an IDbConnection dependency, even if a subclass writes to a file system instead of a database.
Pull non-algorithm concerns out of the base class:
// Prefer: focused base class
public abstract class FileImporter
{
private readonly IDataWriter _writer;
protected FileImporter(IDataWriter writer)
{
_writer = writer
?? throw new ArgumentNullException(nameof(writer));
}
public void Import(string filePath)
{
var data = ReadFile(filePath);
var transformed = TransformData(data);
_writer.Write(transformed);
}
protected abstract RawData ReadFile(string filePath);
protected abstract ProcessedData TransformData(RawData data);
}
Logging moves to a decorator or middleware. Configuration moves to the injected dependencies. Database writes move behind IDataWriter. The base class owns the three-step import algorithm -- read, transform, write -- and nothing more. New subclasses like CsvFileImporter or JsonFileImporter only implement reading and transforming, without inheriting database or logging concerns they didn't ask for.
Testing Template Method Implementations
Testing template method hierarchies demands a strategy that verifies both the algorithm order in the base class and the individual step implementations in each subclass. Three techniques cover these needs.
Verify Step Order with a Test Spy
A test spy subclass records which methods were called and in what order. This verifies that the base class's template method calls steps in the correct sequence:
public sealed class SpyDataExporter : DataExporter
{
public List<string> CallLog { get; } = new();
protected override void Validate(DataSet data)
{
CallLog.Add("Validate");
}
protected override void Transform(DataSet data)
{
CallLog.Add("Transform");
}
protected override void WriteOutput(DataSet data)
{
CallLog.Add("WriteOutput");
}
}
public class DataExporterTests
{
[Fact]
public void Export_CallsStepsInCorrectOrder()
{
var spy = new SpyDataExporter();
var data = new DataSet();
spy.Export(data);
Assert.Equal(
new[] { "Validate", "Transform", "WriteOutput" },
spy.CallLog);
}
}
The spy doesn't test business logic -- it tests the contract that the template method pattern enforces. If someone reorders the steps in the base class, this test catches it.
Test Each Concrete Subclass Independently
Each concrete subclass should have its own tests that verify its step implementations. These tests exercise the real business logic without worrying about the template method's orchestration:
public class CsvExporterTests
{
[Fact]
public void Validate_WithEmptyDataSet_ThrowsArgumentException()
{
var exporter = new CsvExporter();
var emptyData = new DataSet();
Assert.Throws<ArgumentException>(
() => exporter.Export(emptyData));
}
[Fact]
public void Export_WithValidData_ProducesExpectedCsvOutput()
{
var exporter = new CsvExporter();
var data = CreateSampleDataSet();
exporter.Export(data);
var output = exporter.GetOutput();
Assert.Contains("Name,Age,City", output);
Assert.Contains("Alice,30,Seattle", output);
}
}
Verify Hook Invocations
If your base class includes hooks, test that they fire at the expected points. A hook spy subclass captures this:
public sealed class HookSpyOrderProcessor : OrderProcessor
{
public List<string> HookLog { get; } = new();
protected override void Validate(Order order) { }
protected override void ProcessPayment(Order order) { }
protected override void FulfillOrder(Order order) { }
protected override void OnBeforeValidation(Order order)
{
HookLog.Add("OnBeforeValidation");
}
protected override void OnAfterPayment(Order order)
{
HookLog.Add("OnAfterPayment");
}
}
public class OrderProcessorHookTests
{
[Fact]
public void ProcessOrder_InvokesHooksAtCorrectPoints()
{
var spy = new HookSpyOrderProcessor();
var order = new Order { Id = "ORD-001" };
spy.ProcessOrder(order);
Assert.Contains("OnBeforeValidation", spy.HookLog);
Assert.Contains("OnAfterPayment", spy.HookLog);
Assert.True(
spy.HookLog.IndexOf("OnBeforeValidation")
< spy.HookLog.IndexOf("OnAfterPayment"));
}
}
This three-pronged testing approach -- spy for order, independent tests for logic, hook spies for extension points -- gives you full coverage of template method pattern best practices without brittle tests that break when any single subclass changes. The spy pattern here works similarly to the mock-based isolation used when testing strategy pattern implementations.
Integration with Dependency Injection
Template method pattern classes use inheritance, and inheritance-based designs need thoughtful IServiceCollection registration. You can't register an abstract base class directly -- the container needs to know which concrete subclass to resolve.
For a single concrete implementation, registration is straightforward:
services.AddScoped<DataExporter, CsvExporter>();
When multiple subclasses exist and different consumers need different ones, keyed services in .NET 8+ keep registrations explicit:
services.AddKeyedScoped<DataExporter, CsvExporter>("csv");
services.AddKeyedScoped<DataExporter, JsonExporter>("json");
services.AddKeyedScoped<DataExporter, XmlExporter>("xml");
A consumer requests a specific key:
public sealed class ExportController
{
private readonly DataExporter _exporter;
public ExportController(
[FromKeyedServices("csv")] DataExporter exporter)
{
_exporter = exporter;
}
}
For runtime selection -- when the format comes from a configuration value or user input -- a factory delegate gives you flexibility:
services.AddScoped<Func<string, DataExporter>>(sp =>
format => format switch
{
"csv" => sp.GetRequiredKeyedService<DataExporter>("csv"),
"json" => sp.GetRequiredKeyedService<DataExporter>("json"),
"xml" => sp.GetRequiredKeyedService<DataExporter>("xml"),
_ => throw new ArgumentException(
$"Unknown export format: {format}",
nameof(format))
});
One important consideration: if your template method base class constructor takes dependencies, each concrete subclass must pass them through. Keep the base class constructor's parameter list short to avoid painful constructor chains:
public abstract class DataExporter
{
private readonly IDataWriter _writer;
protected DataExporter(IDataWriter writer)
{
_writer = writer
?? throw new ArgumentNullException(nameof(writer));
}
}
public sealed class CsvExporter : DataExporter
{
private readonly CsvOptions _options;
public CsvExporter(
IDataWriter writer,
IOptions<CsvOptions> options)
: base(writer)
{
_options = options.Value;
}
}
The base class takes only what the algorithm needs. Subclass-specific dependencies stay in the subclass constructor. This keeps the template method pattern compatible with DI containers without forcing every subclass to drag along dependencies they don't use. For deeper background on how containers resolve these hierarchies, see the guide on inversion of control.
Frequently Asked Questions
Should the template method always be public?
The template method should match the access level that makes sense for its consumers. In most cases, public is correct because external code calls the algorithm. If only other classes within the same assembly call it, internal works. The key constraint is that the template method should not be virtual or abstract -- its access modifier is about visibility, while preventing overrides is about protecting the algorithm's structure.
How many hooks are too many in a template method base class?
If you have more hooks than abstract methods, reconsider whether the template method pattern is the right fit. A base class with three abstract steps and eight hooks creates a confusing surface area where subclass authors don't know which hooks matter. Keep hooks to the natural boundaries of your algorithm -- before and after the major phases. If you need fine-grained extensibility at many points, a pipeline or observer-based approach may serve you better.
Can I combine the template method pattern with the strategy pattern?
Yes, and this is a strong approach for reducing rigid inheritance hierarchies. The template method defines the algorithm structure, while individual steps can delegate to injected strategy objects. Instead of creating a new subclass for every variation, you compose behaviors by injecting different strategies into the same concrete class. This reduces the number of subclasses you need to maintain.
What is the difference between a hook and an abstract method?
An abstract method forces every subclass to provide an implementation -- it represents a step the algorithm cannot run without. A hook is a virtual method with a default implementation (often empty) that subclasses may optionally override. Use abstract methods for mandatory variation points and hooks for optional extension points. If a subclass frequently leaves a method body empty, that method should be a hook rather than an abstract requirement.
How do I prevent subclasses from calling base class steps out of order?
Mark the template method as non-virtual and make the step methods protected. This means external code can only call the template method, which enforces the correct sequence. Subclasses can override individual steps but cannot call them directly on the base class to reorder execution. The protected visibility ensures the steps are only accessible within the hierarchy, and the non-virtual template method ensures the orchestration stays fixed.
When should I choose the template method pattern over the bridge pattern?
The template method pattern works best when you have a fixed algorithm with steps that vary by subclass. The bridge pattern separates an abstraction from its implementation so both sides can vary independently. If you need a single algorithm skeleton with interchangeable steps, template method fits. If you need two independent hierarchies that combine in multiple ways, the bridge is the better choice.
How do I handle exceptions in template method steps?
Add error-handling hooks that fire when a step fails. The base class can catch exceptions from abstract steps and delegate them to an OnStepFailed hook before deciding whether to rethrow or continue. This gives subclasses a chance to log, compensate, or clean up without forcing them to wrap every step in try-catch blocks.
Wrapping Up Template Method Pattern Best Practices
Applying these template method pattern best practices in C# keeps your inheritance hierarchies from becoming maintenance liabilities. The core principles: seal the template method to protect algorithm order, minimize abstract methods to reduce subclass burden, design hooks for optional extensibility, and keep the base class focused on the algorithm skeleton.
Start with the simplest template method that captures your algorithm's invariant structure -- a non-virtual method calling two or three abstract steps. Add hooks only when subclasses demonstrate a real need for extension points. Register concrete subclasses through keyed services or factory delegates when multiple implementations coexist. Test algorithm order with spy subclasses and test step logic independently in each concrete implementation. The goal is an inheritance hierarchy that's easy to extend with new subclasses and hard to break by accident.

