Back to Blog
design-patternscsharptypescriptcreational-patterns

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 RecurringJobTemplate is 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 JobCardBuilder from 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.