Prototype Pattern — FixIt Pro Series #04
FixIt Pro handymen run the same jobs every month. Learn how the Prototype pattern clones recurring job card templates instantly — without re-running the full build chain every time — in C# and TypeScript.
Series: Design Patterns with FixIt Pro · Episode 04 / 22 · Creational Pattern
Previous: #03 — Builder Pattern
The Scenario
FixIt Pro has been running for a while. Patterns emerge.
Every first Monday of the month, the same estate agent books a boiler service across 12 units. Same category. Same parts list. Same special instructions. Same priority. Only the unit number and scheduled date change.
Every week, the same property manager books a fire extinguisher inspection across an office park. Again — almost identical job cards, every single time.
Right now, the dispatcher builds each card from scratch using the JobCardBuilder from Episode 3. That works, but it's repetitive — the same fluent chain, copy-pasted or wrapped in a helper, with minor tweaks per instance.
The Prototype pattern gives you a better tool: define the job card once as a template, then clone it. Tweak the clone. Done.
What Is the Prototype Pattern?
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying that prototype.
Instead of constructing from scratch, you copy an existing object and modify only what needs to change. The original template is untouched.
In FixIt Pro terms:
- A
RecurringJobTemplateis configured once — fully built, validated, ready - When a recurring job is due, you clone the template
- Override the unit number and scheduled date on the clone
- Issue it — zero re-configuration of parts, instructions, priority, or inspection flags
The two participants
| Role | FixIt Pro equivalent |
|---|---|
| Prototype interface | ICloneable<T> — declares a Clone() method |
| Concrete Prototype | JobCard — implements deep cloning of itself |
There's also a Prototype Registry — a store of named templates you can retrieve and clone by key. This is where the pattern gets practically useful.
C# Implementation
// ── Cloneable JobCard ──────────────────────────────────────
// We extend the JobCard from Episode 3 with a Clone() method.
// Note: C# has a built-in IClonable interface but it returns object,
// so we define a typed generic version for clarity.
public interface IPrototype<T>
{
T Clone();
}
public class JobCard : IPrototype<JobCard>
{
public string JobId { get; init; } = Guid.NewGuid().ToString()[..8];
public string Title { get; set; } = string.Empty;
public string Category { get; init; } = string.Empty;
public string Priority { get; set; } = "NORMAL";
public DateTime? ScheduledAt { get; set; }
public List<string> Parts { get; init; } = new();
public List<string> PhotoUrls { get; init; } = new();
public string? SpecialInstructions { get; set; }
public bool RequiresInspection { get; init; }
public bool HasWarranty { get; init; }
public string? UnitReference { get; set; }
// Deep clone — new JobId, new lists, same values
public JobCard Clone() => new JobCard
{
// JobId is intentionally regenerated — each clone is a new job
Title = Title,
Category = Category,
Priority = Priority,
ScheduledAt = ScheduledAt,
Parts = new List<string>(Parts), // deep copy
PhotoUrls = new List<string>(PhotoUrls), // deep copy
SpecialInstructions = SpecialInstructions,
RequiresInspection = RequiresInspection,
HasWarranty = HasWarranty,
UnitReference = UnitReference,
};
public void PrintSummary()
{
Console.WriteLine($"\n=== Job Card #{JobId} ===");
Console.WriteLine($" Title : {Title}");
Console.WriteLine($" Category : {Category}");
Console.WriteLine($" Priority : {Priority}");
Console.WriteLine($" Unit : {UnitReference ?? "N/A"}");
if (ScheduledAt.HasValue)
Console.WriteLine($" Scheduled: {ScheduledAt:dd MMM yyyy HH:mm}");
if (Parts.Any())
Console.WriteLine($" Parts : {string.Join(", ", Parts)}");
if (!string.IsNullOrEmpty(SpecialInstructions))
Console.WriteLine($" Notes : {SpecialInstructions}");
Console.WriteLine($" Inspection Required : {RequiresInspection}");
Console.WriteLine($" Warranty : {HasWarranty}");
}
}
// ── Prototype Registry ─────────────────────────────────────
public class JobCardTemplateRegistry
{
private readonly Dictionary<string, JobCard> _templates = new();
public void Register(string key, JobCard template) =>
_templates[key] = template;
public JobCard CloneTemplate(string key)
{
if (!_templates.TryGetValue(key, out var template))
throw new KeyNotFoundException($"No template registered for key '{key}'.");
return template.Clone();
}
}
// ── Client Code ────────────────────────────────────────────
class Program
{
static void Main()
{
// ── Build templates once ───────────────────────────
var boilerServiceTemplate = new JobCardBuilder("Monthly Boiler Service", "Plumbing")
.WithPriority("NORMAL")
.WithParts("descaling solution", "pressure gauge", "service report form")
.WithInstructions("Annual boiler service — check pressure relief valve.")
.WithWarranty()
.Build();
var fireInspectionTemplate = new JobCardBuilder("Weekly Fire Extinguisher Inspection", "Safety")
.WithPriority("HIGH")
.RequiresSafetyInspection()
.WithInstructions("Check pressure, pin, and tamper seal. Tag each unit.")
.Build();
// ── Register in the template registry ─────────────
var registry = new JobCardTemplateRegistry();
registry.Register("boiler-service", boilerServiceTemplate);
registry.Register("fire-inspection", fireInspectionTemplate);
// ── Clone for each recurring job instance ──────────
Console.WriteLine("--- Issuing Monthly Boiler Services ---");
var units = new[] { "Unit 1A", "Unit 2B", "Unit 3C" };
var serviceDate = new DateTime(2025, 6, 2, 8, 0, 0);
foreach (var unit in units)
{
var card = registry.CloneTemplate("boiler-service");
card.UnitReference = unit;
card.ScheduledAt = serviceDate;
card.PrintSummary();
serviceDate = serviceDate.AddHours(2); // stagger slots
}
// ── Clone for weekly fire inspection ──────────────
Console.WriteLine("\n--- Issuing Weekly Fire Inspections ---");
var offices = new[] { "Block A", "Block B" };
var inspectionDate = new DateTime(2025, 6, 3, 9, 0, 0);
foreach (var office in offices)
{
var card = registry.CloneTemplate("fire-inspection");
card.UnitReference = office;
card.ScheduledAt = inspectionDate;
card.PrintSummary();
inspectionDate = inspectionDate.AddHours(1);
}
}
}
Output:
--- Issuing Monthly Boiler Services ---
=== Job Card #f3a12b9c ===
Title : Monthly Boiler Service
Category : Plumbing
Priority : NORMAL
Unit : Unit 1A
Scheduled: 02 Jun 2025 08:00
Parts : descaling solution, pressure gauge, service report form
Notes : Annual boiler service — check pressure relief valve.
Inspection Required : False
Warranty : True
=== Job Card #a7d45e01 ===
Title : Monthly Boiler Service
...
Unit : Unit 2B
Scheduled: 02 Jun 2025 10:00
...
TypeScript Implementation
// ── Product/JobCard.ts ─────────────────────────────────────
const shortId = () => Math.random().toString(36).slice(2, 10);
export interface IPrototype<T> {
clone(): T;
}
export class JobCard implements IPrototype<JobCard> {
readonly jobId: string = shortId();
title: string;
readonly category: string;
priority: string;
scheduledAt?: Date;
readonly parts: string[];
readonly photoUrls: string[];
specialInstructions?: string;
readonly requiresInspection: boolean;
readonly hasWarranty: boolean;
unitReference?: string;
constructor(config: {
title: string;
category: string;
priority: string;
scheduledAt?: Date;
parts: string[];
photoUrls: string[];
specialInstructions?: string;
requiresInspection: boolean;
hasWarranty: boolean;
unitReference?: string;
}) {
this.title = config.title;
this.category = config.category;
this.priority = config.priority;
this.scheduledAt = config.scheduledAt;
this.parts = config.parts;
this.photoUrls = config.photoUrls;
this.specialInstructions = config.specialInstructions;
this.requiresInspection = config.requiresInspection;
this.hasWarranty = config.hasWarranty;
this.unitReference = config.unitReference;
}
// Deep clone — new jobId, copied arrays, same values
clone(): JobCard {
return new JobCard({
// jobId is regenerated automatically by the constructor
title: this.title,
category: this.category,
priority: this.priority,
scheduledAt: this.scheduledAt ? new Date(this.scheduledAt) : undefined,
parts: [...this.parts], // spread = shallow copy of primitives ✓
photoUrls: [...this.photoUrls],
specialInstructions: this.specialInstructions,
requiresInspection: this.requiresInspection,
hasWarranty: this.hasWarranty,
unitReference: this.unitReference,
});
}
printSummary(): void {
console.log(`\n=== Job Card #${this.jobId} ===`);
console.log(` Title : ${this.title}`);
console.log(` Category : ${this.category}`);
console.log(` Priority : ${this.priority}`);
console.log(` Unit : ${this.unitReference ?? "N/A"}`);
if (this.scheduledAt)
console.log(` Scheduled: ${this.scheduledAt.toLocaleString()}`);
if (this.parts.length)
console.log(` Parts : ${this.parts.join(", ")}`);
if (this.specialInstructions)
console.log(` Notes : ${this.specialInstructions}`);
console.log(` Inspection Required : ${this.requiresInspection}`);
console.log(` Warranty : ${this.hasWarranty}`);
}
}
// ── Prototype/JobCardTemplateRegistry.ts ──────────────────
import { JobCard } from "../Product/JobCard";
export class JobCardTemplateRegistry {
private templates = new Map<string, JobCard>();
register(key: string, template: JobCard): void {
this.templates.set(key, template);
}
cloneTemplate(key: string): JobCard {
const template = this.templates.get(key);
if (!template)
throw new Error(`No template registered for key '${key}'.`);
return template.clone();
}
}
// ── App.ts ─────────────────────────────────────────────────
import { JobCardBuilder } from "./Concrete Builder/JobCardBuilder";
import { JobCardTemplateRegistry } from "./Prototype/JobCardTemplateRegistry";
// ── Build templates once ───────────────────────────────────
const boilerServiceTemplate = new JobCardBuilder("Monthly Boiler Service", "Plumbing")
.withPriority("NORMAL")
.withParts("descaling solution", "pressure gauge", "service report form")
.withInstructions("Annual boiler service — check pressure relief valve.")
.withWarranty()
.build();
const fireInspectionTemplate = new JobCardBuilder("Weekly Fire Extinguisher Inspection", "Safety")
.withPriority("HIGH")
.requiresSafetyInspection()
.withInstructions("Check pressure, pin, and tamper seal. Tag each unit.")
.build();
// ── Register in the template registry ─────────────────────
const registry = new JobCardTemplateRegistry();
registry.register("boiler-service", boilerServiceTemplate);
registry.register("fire-inspection", fireInspectionTemplate);
// ── Clone for each recurring job instance ─────────────────
console.log("--- Issuing Monthly Boiler Services ---");
const units = ["Unit 1A", "Unit 2B", "Unit 3C"];
let serviceDate = new Date("2025-06-02T08:00:00");
for (const unit of units) {
const card = registry.cloneTemplate("boiler-service");
card.unitReference = unit;
card.scheduledAt = new Date(serviceDate);
card.printSummary();
serviceDate.setHours(serviceDate.getHours() + 2); // stagger slots
}
// ── Clone for weekly fire inspection ──────────────────────
console.log("\n--- Issuing Weekly Fire Inspections ---");
const offices = ["Block A", "Block B"];
let inspectionDate = new Date("2025-06-03T09:00:00");
for (const office of offices) {
const card = registry.cloneTemplate("fire-inspection");
card.unitReference = office;
card.scheduledAt = new Date(inspectionDate);
card.printSummary();
inspectionDate.setHours(inspectionDate.getHours() + 1);
}
C# vs TypeScript — Key Differences
| Aspect | C# | TypeScript |
|---|---|---|
| Clone interface | Custom IPrototype<T> (typed) |
Custom IPrototype<T> (same) |
| Built-in clone | IClonable exists but returns object — avoid |
structuredClone() exists but loses class methods |
| List deep copy | new List<string>(original) |
[...original] spread operator |
| Date deep copy | DateTime is a value type — copies automatically |
new Date(original) — reference type, must copy manually |
| Mutable fields | Mix of init (immutable) and set (mutable) |
Mix of readonly and mutable fields |
| Registry | Dictionary<string, JobCard> |
Map<string, JobCard> |
The Date deep copy difference is a genuine gotcha in TypeScript. Date is a reference type — if you do scheduledAt: this.scheduledAt in clone(), both the original and the clone point to the same Date object. Mutating one mutates both. Always use new Date(this.scheduledAt) when cloning.
Shallow Clone vs Deep Clone
This is the most important concept in the Prototype pattern and the most common source of bugs.
| Shallow Clone | Deep Clone | |
|---|---|---|
| What it copies | Top-level field values | All nested objects recursively |
Primitive fields (string, number, bool) |
✅ Safe — copied by value | ✅ Same |
Reference fields (List, Date, objects) |
❌ Dangerous — shared reference | ✅ Safe — new independent copy |
| FixIt Pro risk | Mutating Parts on clone mutates the template |
No risk — Parts is a new list |
Always deep clone mutable reference fields. In FixIt Pro, Parts and PhotoUrls are List<string> / string[] — both cloned with new List<>(original) / [...original] respectively.
When to Use the Prototype
Use it when:
- Object creation is expensive and you want to copy a pre-configured instance instead
- You have many variations of an object that differ only slightly from a base template
- You want a registry of reusable configurations your system can clone on demand
- You need to snapshot an object's state without knowing its concrete class (pairs well with Memento — Episode 17)
Avoid it when:
- Your objects are simple to construct — cloning adds complexity without benefit
- Deep cloning is complicated by circular references or non-copyable resources (file handles, DB connections)
Real-World Takeaway
Object.assign() and the spread operator { ...obj } in JavaScript are shallow prototype clones. JSON.parse(JSON.stringify(obj)) is a crude deep clone — functional but lossy (drops undefined, Date becomes a string, functions disappear). For production TypeScript, a proper clone() method like the one above — or a library like structuredClone for plain objects — is the right call.
In .NET, MemberwiseClone() gives you a shallow clone for free. For deep cloning, you implement it explicitly — exactly as we did with IPrototype<T>.
In FixIt Pro, the Prototype pattern means the operations team can register 20 recurring job templates once, and the scheduler clones them every week without a single call to JobCardBuilder. The templates live in the registry. The clones go out the door.
Repo Structure for This Episode
github.com/antonlungameni/fixit-pro-design-patterns
fixit-pro-design-patterns/
├── csharp/Creational/04-Prototype/
│ ├── Product/JobCard.cs
│ ├── Prototype/IPrototype.cs
│ ├── Prototype/JobCardTemplateRegistry.cs
│ └── Program.cs
└── typescript/Creational/04-Prototype/
├── Product/JobCard.ts
├── Prototype/JobCardTemplateRegistry.ts
└── App.ts
This episode reuses
JobCardBuilderfrom Episode 3. The C# project references the builder inline; the TypeScript project imports it directly.
Previous: #03 — Builder Pattern
Next up: #05 — Singleton Pattern
One JobCardRegistry to rule them all — ensuring a single, globally consistent store of active job cards across the entire FixIt Pro system.