Tea and coffee can share a preparation sequence while varying the brewing step. The sequence belongs in one place. Subclasses fill the deliberate gaps.
Template Method puts an algorithm’s control flow in a base class. Its public method calls shared steps plus a small set of abstract or virtual hooks. ReportGenerator.GenerateAsync() can own fetching, validation, auditing, and result assembly while subclasses provide only the output format. The base class controls when each hook runs, so the extension points form a protocol rather than a collection of unrelated overrides.
sequenceDiagram participant Client participant Base as ReportGenerator participant Sub as PdfReportGenerator Client->>Base: Generate Base->>Base: FetchData - shared Base->>Base: ValidateData - shared Base->>Sub: FormatReport - overridden Base->>Sub: WriteOutput - overridden Sub-->>Client: Report complete
Problem
PdfReportGenerator, CsvReportGenerator, and ExcelReportGenerator each independently implement the same fetch → validate → format → write lifecycle, duplicating orchestration logic:
public class PdfReportGenerator
{
public async Task<byte[]> GenerateAsync(Guid orderId)
{
// ⚠️ Fetch, validate, and audit logic duplicated in every generator
var order = await _repository.GetAsync(orderId);
if (order is null) throw new NotFoundException(orderId);
await _auditLog.RecordAsync($"Report generated for order {orderId}");
// Format-specific logic
var pdf = new PdfDocument();
pdf.AddPage().AddTable(order.Items.Select(i => new[] { i.ProductId.ToString(), i.Quantity.ToString() }));
return pdf.Save();
}
}
public class CsvReportGenerator
{
public async Task<byte[]> GenerateAsync(Guid orderId)
{
// ⚠️ Same fetch/validate/audit — copy-pasted
var order = await _repository.GetAsync(orderId);
if (order is null) throw new NotFoundException(orderId);
await _auditLog.RecordAsync($"Report generated for order {orderId}");
// Format-specific logic
var sb = new StringBuilder("ProductId,Quantity,UnitPrice\n");
foreach (var item in order.Items)
sb.AppendLine($"{item.ProductId},{item.Quantity},{item.UnitPrice}");
return Encoding.UTF8.GetBytes(sb.ToString());
}
}
// ⚠️ Adding ExcelReportGenerator = copy-paste the fetch/validate/audit block againAdding an audit field now requires the same edit in every generator, and one missed copy produces a different workflow for that format.
Solution
ReportGenerator base class defines the algorithm skeleton. Subclasses override only the format-specific steps:
// Abstract base — defines the template method
public abstract class ReportGenerator
{
protected ReportGenerator(IOrderRepository repository, IAuditLog auditLog)
{
Repository = repository;
AuditLog = auditLog;
}
// ✅ Template method — sealed, defines the algorithm skeleton
public async Task<Report> GenerateAsync(Guid orderId)
{
var order = await FetchDataAsync(orderId); // step 1: always the same
ValidateData(order); // step 2: always the same
var content = await FormatReportAsync(order); // step 3: subclass-specific
await RecordAuditAsync(orderId); // step 4: record successful formatting
return new Report(GetContentType(), content); // step 5: uses subclass value
}
// Fixed steps — shared implementation
private async Task<Order> FetchDataAsync(Guid orderId)
{
var order = await Repository.GetAsync(orderId);
return order ?? throw new NotFoundException(orderId);
}
private void ValidateData(Order order)
{
if (order.Items.Count == 0)
throw new InvalidOperationException("Cannot generate report for empty order");
}
private Task RecordAuditAsync(Guid orderId) =>
AuditLog.RecordAsync($"Report generated for order {orderId} as {GetContentType()}");
// Abstract steps — subclasses must implement
protected abstract Task<byte[]> FormatReportAsync(Order order);
protected abstract string GetContentType();
protected IOrderRepository Repository { get; }
protected IAuditLog AuditLog { get; }
}
// Concrete implementations — override only format-specific steps
public class PdfReportGenerator(IOrderRepository repository, IAuditLog auditLog)
: ReportGenerator(repository, auditLog)
{
protected override Task<byte[]> FormatReportAsync(Order order)
{
var pdf = new PdfDocument();
var page = pdf.AddPage();
page.AddHeading($"Order #{order.Id}");
page.AddTable(order.Items.Select(i => new[] { i.ProductId.ToString(), i.Quantity.ToString(), i.UnitPrice.ToString("C") }));
return Task.FromResult(pdf.Save());
}
protected override string GetContentType() => "application/pdf";
}
public class CsvReportGenerator(IOrderRepository repository, IAuditLog auditLog)
: ReportGenerator(repository, auditLog)
{
protected override Task<byte[]> FormatReportAsync(Order order)
{
var sb = new StringBuilder("ProductId,Quantity,UnitPrice\n");
foreach (var item in order.Items)
sb.AppendLine($"{item.ProductId},{item.Quantity},{item.UnitPrice:F2}");
return Task.FromResult(Encoding.UTF8.GetBytes(sb.ToString()));
}
protected override string GetContentType() => "text/csv";
}
// ✅ Adding Excel = new subclass, zero changes to base class or other generators
public class ExcelReportGenerator(IOrderRepository repository, IAuditLog auditLog)
: ReportGenerator(repository, auditLog)
{
protected override Task<byte[]> FormatReportAsync(Order order)
{
using var workbook = new XLWorkbook();
var sheet = workbook.AddWorksheet("Order");
// ... populate Excel
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return Task.FromResult(stream.ToArray());
}
protected override string GetContentType() =>
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
}GenerateAsync() is non-virtual, so subclasses cannot override the control flow. The inline “sealed” comment describes that intent. sealed is not a modifier on this method.
An Excel generator supplies only the format-specific hooks. Fetching, validation, and auditing still follow the base algorithm.
Framework examples
BackgroundService.ExecuteAsync() is the required hook invoked by the base service lifecycle. The host owns registration and calls StartAsync() and StopAsync(). The derived service supplies its long-running operation and must honor cancellation.
Stream fixes high-level copying behavior while derived streams provide their supported I/O operations. Capability flags such as CanRead and CanSeek are part of the contract because not every stream supports every hook.
DbContext.OnModelCreating() is a hook inside EF Core’s model-building process. A derived context contributes mappings without owning when the model is initialized or cached.
AuthenticationHandler<T>.HandleAuthenticateAsync() supplies scheme-specific authentication inside a base handler that coordinates the surrounding authentication operation.
Tradeoffs
Template Method fits variants that share a stable algorithm and differ at a few intentional hooks. It is an inversion-of-control mechanism: the base class calls overridden steps at times chosen by the base algorithm (see IoC).
It fits poorly when behavior must change at runtime or when inheritance exists only to reuse a few lines. A base-class change can affect every subtype through undocumented hook order, shared state, or new assumptions.
Software Architecture/Patterns/Design Patterns/Behavioral/Strategy moves variation into composed objects or delegates, which allows runtime replacement and avoids consuming the single base-class slot. Template Method is smaller when the base genuinely owns most of the workflow. When two dimensions vary independently, Software Architecture/Patterns/Design Patterns/Structural/Bridge can keep them from turning into a cross-product of subclasses.
Questions
What should determine whether algorithm variation uses Template Method or Strategy?
The decision depends on who owns the algorithm and when the variation must be chosen. Template Method fits when a base class owns a stable sequence and subclasses vary a few steps. Strategy fits when the varying behavior is a separate collaborator that must be selected or replaced independently, including at runtime.
Aspect Template Method Strategy Who owns the algorithm The base class runs the sequence and calls hooks that subclasses override. The context calls a composed object or delegate that supplies the varying behavior. When a variant is chosen When a subclass is instantiated, so the choice is fixed for that object. When a strategy is passed in or selected, so it can change at runtime. Coupling Subclasses depend on the base class’s hook order and shared state, so a base-class change can affect every subtype. Inheritance also uses the class’s single base-class slot. Variation stays outside the class hierarchy, so the base-class slot remains free. Cost Smallest design when the base genuinely owns most of the workflow. Adds a separate abstraction and, with several strategies, logic to select among them. Template Method fits poorly when behavior must change at runtime or when inheritance exists only to reuse a few lines.
What is the "Hollywood Principle," and how does Template Method apply it?
The Hollywood Principle means the framework or base class owns the control flow and calls supplied extension points. Template Method applies it by fixing the algorithm’s sequence while subclasses implement selected hooks; subclasses follow that protocol instead of driving the whole operation. Template Method implementation