You're building a document editor. Users can create templates: page layout, fonts, margins, headers, footers, watermarks, paragraph styles. A single template object has 30+ properties and nested child objects.
When a user clicks "New Document from Template", you need a fresh copy. Not a reference to the template. A completely independent copy they can modify without affecting the original.
Building this from scratch every time means re-reading the template from the database, re-applying all the styles, re-building the nested objects. That's slow and fragile.
What if you could just... copy the existing object?
Here's the brute-force approach:
public class DocumentService{ private readonly ITemplateRepository _repo; public async Task<Document> CreateFromTemplateAsync(int templateId) { var template = await _repo.GetByIdAsync(templateId); // Manually copying 30+ properties var doc = new Document { Title = "Untitled", PageWidth = template.PageWidth, PageHeight = template.PageHeight, MarginTop = template.MarginTop, MarginBottom = template.MarginBottom, MarginLeft = template.MarginLeft, MarginRight = template.MarginRight, Font = template.Font, FontSize = template.FontSize, HeaderText = template.HeaderText, FooterText = template.FooterText, Watermark = template.Watermark, // 20 more properties... Paragraphs = template.Paragraphs .Select(p => new Paragraph { Text = p.Text, Style = p.Style, Alignment = p.Alignment // More nested copying... }) .ToList() }; return doc; }}
Every time the template gets a new property, this method must be updated. Miss one? Silent bug. And the nested object copying is a minefield for shallow copy mistakes where both documents share the same Paragraph list.
The Prototype pattern lets you copy existing objects without depending on their concrete classes. The object itself knows how to clone itself, including all its nested state.
Define a prototype interface and a self-cloning document:
// Prototype interfacepublic interface IPrototype<T>{ T DeepClone();} public class Document : IPrototype<Document>{ public string Title { get; set; } = string.Empty; public double PageWidth { get; set; } public double PageHeight { get; set; } public Margins Margins { get; set; } = new(); public FontSettings Font { get; set; } = new(); public string? HeaderText { get; set; } public string? FooterText { get; set; } public WatermarkSettings? Watermark { get; set; } public List<Paragraph> Paragraphs { get; set; } = new(); public Document DeepClone() { return new Document { Title = Title, PageWidth = PageWidth, PageHeight = PageHeight, // Clone nested objects - not just copy references Margins = Margins.DeepClone(), Font = Font.DeepClone(), HeaderText = HeaderText, FooterText = FooterText, Watermark = Watermark?.DeepClone(), Paragraphs = Paragraphs.Select(p => p.DeepClone()).ToList() }; }} public class Margins : IPrototype<Margins>{ public double Top { get; set; } public double Bottom { get; set; } public double Left { get; set; } public double Right { get; set; } public Margins DeepClone() => new() { Top = Top, Bottom = Bottom, Left = Left, Right = Right };} public class Paragraph : IPrototype<Paragraph>{ public string Text { get; set; } = string.Empty; public ParagraphStyle Style { get; set; } = new(); public TextAlignment Alignment { get; set; } public Paragraph DeepClone() => new() { Text = Text, Style = Style.DeepClone(), Alignment = Alignment };}
Now creating from a template is trivial:
public class DocumentService{ private readonly ITemplateRepository _repo; public async Task<Document> CreateFromTemplateAsync(int templateId) { var template = await _repo.GetByIdAsync(templateId); // One line. Deep clone handles everything. var doc = template.DeepClone(); doc.Title = "Untitled"; return doc; }}
The template knows how to clone itself, including all nested objects. The service doesn't need to know the internal structure.
Encapsulated cloning logic. The object owns its copy logic. Adding a new property means updating one class, not every place that creates copies.
Deep copy safety. Each nested object clones itself. No shared references, no mutation bugs.
Works with any object graph. Doesn't matter how deep the nesting goes. Each level handles its own cloning.
Cache commonly used prototypes so you don't hit the database every time:
public class TemplateRegistry{ private readonly ConcurrentDictionary<string, Document> _templates = new(); public void Register(string name, Document template) { _templates[name] = template; } public Document Create(string templateName) { if (!_templates.TryGetValue(templateName, out var template)) throw new KeyNotFoundException($"Template '{templateName}' not found"); // Always return a clone, never the original return template.DeepClone(); }} // Register at startupregistry.Register("invoice", invoiceTemplate);registry.Register("report", reportTemplate);registry.Register("letter", letterTemplate); // Create documents instantly from cached prototypesvar invoice = registry.Create("invoice");
The registry holds live prototypes in memory. Creating a new document is a clone operation — no database hit, no complex construction.
For objects where writing manual DeepClone() is impractical, use serialization:
public static class CloneExtensions{ public static T DeepClone<T>(this T source) where T : class { var json = JsonSerializer.Serialize(source); return JsonSerializer.Deserialize<T>(json)!; }} // Works on any serializable objectvar clone = complexObject.DeepClone();
This is slower than manual cloning but requires zero boilerplate. Good for objects that change frequently or have massive property counts. Just make sure all properties are serializable.
C# records with with expressions give you a built-in shallow prototype mechanism:
public record OrderTemplate( string Region, string Currency, decimal TaxRate, string ShippingMethod, List<string> DefaultTags); var usTemplate = new OrderTemplate("US", "USD", 0.08m, "USPS", new() { "domestic" }); // 'with' creates a shallow copy with overridesvar caTemplate = usTemplate with { Region = "CA", Currency = "CAD", TaxRate = 0.13m };
Be careful: with on records is a shallow copy. The DefaultTags list is shared between both instances. For deep copies of collections, you still need manual cloning or the serialization approach.
When objects are simple to create. If construction is just setting 3-4 properties, new is simpler and more obvious than cloning.
When objects have circular references. Deep cloning with circular dependencies is tricky and error-prone. You'll need custom logic or serialization that handles cycles.
When objects hold unmanaged resources. Cloning a database connection or file handle doesn't make sense. The Prototype pattern works best for pure data objects.
with expressions provide lightweight shallow prototypesThe Prototype pattern creates new objects by copying existing ones. Instead of constructing from scratch with new, you clone a fully configured object and customize the copy. The object itself handles the cloning logic.
Use it when object creation is expensive (database calls, complex setup), when you need copies of complex objects with deep nesting, or when you want to create variations of a preconfigured template without repeating construction logic.
For simple objects with a few properties and no nesting, yes. Just use new. The pattern earns its keep when objects are deeply nested, expensive to construct, or frequently duplicated from templates.
The Builder pattern works when you want step-by-step construction. Copy constructors are a simpler alternative for flat objects. C# records with with expressions offer lightweight shallow copying.
The Prototype pattern is more common than most developers realize. Every time you copy an Excel template, duplicate a Kubernetes deployment config, or fork a Git repo, you're using the Prototype pattern. In .NET, it shows up in document generation, configuration management, and game development.
The key decision is shallow vs. deep clone. Get that wrong and you'll spend hours debugging shared state bugs. Get it right and object creation becomes trivial.
That's all from me today.
P.S. Follow me on YouTube.
If you made it this far, you're clearly serious about writing better .NET code. Here's a 20% discount code: DEEP20 for Design Patterns that Deliver. Consider it a thank-you for actually reading the whole thing.
Here are 2 ebooks I have about design patterns:
Design Patterns that Deliver — 5 essential patterns (Builder, Decorator, Strategy, Adapter, Mediator) with production-ready C# code and real-world examples. Or try a free chapter on the Builder Pattern first.
Design Patterns Simplified — A beginner-friendly guide to understanding design patterns without the academic fluff.
Stop arguing about code style. In this course you get a production-proven setup with analyzers, CI quality gates, and architecture tests — the exact system I use in real projects. Join here.
Not sure yet? Grab the free Starter Kit — a drop-in setup with the essentials from Module 01.
Design Patterns that Deliver — Solve real problems with 5 battle-tested patterns (Builder, Decorator, Strategy, Adapter, Mediator) using practical, real-world examples. Trusted by 650+ developers.
Just getting started? Design Patterns Simplified covers 10 essential patterns in a beginner-friendly, 30-page guide for just $9.95.
Every Monday morning, I share 1 actionable tip on C#, .NET & Architecture that you can use right away. Join here.
Join 20,000+ subscribers who mass-improve their .NET skills with actionable tips on C#, Software Architecture & Best Practices.
Subscribe to the TheCodeMan.net and be among the 20,000+ subscribers gaining practical tips and resources to enhance your .NET expertise.