Back to Blog
design-patternscsharptypescriptcreational-patterns

Factory Method Pattern — FixIt Pro Series #01

Stop hardcoding job card types. Learn how the Factory Method pattern keeps your dispatch system open for extension — in C# and TypeScript.

Series: Design Patterns with FixIt Pro  ·  Episode 01 / 22  ·  Creational Pattern


The Scenario

FixIt Pro is a platform that connects homeowners with qualified handymen. Every repair request becomes a job card — a structured document that captures the work to be done, the priority, and the requirements for the assigned handyman.

Job cards aren't all the same. A plumbing job needs a licensed plumber and a water shutoff check. An electrical job requires a certified electrician and a safety inspection. A carpentry job needs a material estimate. Each type carries its own rules.

So the question becomes: how do we create them cleanly?


The Problem

On day one, you write something like this:

if (type == "plumbing")        card = new PlumbingJobCard(title);
else if (type == "electrical") card = new ElectricalJobCard(title);
else if (type == "carpentry")  card = new CarpentryJobCard(title);

It works. Then you add 8 more job types, and that if/else chain starts appearing in 6 different files — the dispatcher, the API controller, the report generator, the test suite. Add a new category and you're hunting down every occurrence. Change a constructor signature and everything breaks at once.

The root issue is tight coupling: your client code depends directly on concrete classes. The Factory Method pattern exists to break exactly this dependency.


What Is the Factory Method Pattern?

Define an interface for creating an object, but let subclasses decide which class to instantiate. The Factory Method defers instantiation to subclasses.

It has four participants:

Role FixIt Pro equivalent
Product IJobCard — the interface all job cards implement
Concrete Products PlumbingJobCard, ElectricalJobCard, CarpentryJobCard
Creator JobCardCreator — abstract class declaring the factory method
Concrete Creators PlumbingJobCreator, ElectricalJobCreator, CarpentryJobCreator

Adding a new job type means creating one new class — zero changes to existing code. That's the Open/Closed Principle in practice.


C# Implementation

// ── Product Interface ──────────────────────────────────────
public interface IJobCard
{
    string JobId     { get; }
    string Title     { get; }
    string Category  { get; }
    string Priority  { get; }
    void   PrintSummary();
}

// ── Concrete Products ──────────────────────────────────────
public class PlumbingJobCard : IJobCard
{
    public string JobId    { get; } = Guid.NewGuid().ToString()[..8];
    public string Title    { get; }
    public string Category => "Plumbing";
    public string Priority { get; }

    public PlumbingJobCard(string title, string priority)
    {
        Title    = title;
        Priority = priority;
    }

    public void PrintSummary() =>
        Console.WriteLine(
            $"[{Category}] #{JobId} — {Title} (Priority: {Priority})" +
            $"\n  → Requires: Licensed plumber + water shutoff check");
}

public class ElectricalJobCard : IJobCard
{
    public string JobId    { get; } = Guid.NewGuid().ToString()[..8];
    public string Title    { get; }
    public string Category => "Electrical";
    public string Priority { get; }

    public ElectricalJobCard(string title, string priority)
    {
        Title    = title;
        Priority = priority;
    }

    public void PrintSummary() =>
        Console.WriteLine(
            $"[{Category}] #{JobId} — {Title} (Priority: {Priority})" +
            $"\n  → Requires: Certified electrician + safety inspection");
}

public class CarpentryJobCard : IJobCard
{
    public string JobId    { get; } = Guid.NewGuid().ToString()[..8];
    public string Title    { get; }
    public string Category => "Carpentry";
    public string Priority { get; }

    public CarpentryJobCard(string title, string priority)
    {
        Title    = title;
        Priority = priority;
    }

    public void PrintSummary() =>
        Console.WriteLine(
            $"[{Category}] #{JobId} — {Title} (Priority: {Priority})" +
            $"\n  → Requires: Carpenter + material estimate");
}

// ── Creator ────────────────────────────────────────────────
public abstract class JobCardCreator
{
    // The Factory Method
    public abstract IJobCard CreateJobCard(string title, string priority);

    // Template method — uses the factory method internally
    public IJobCard IssueJobCard(string title, string priority)
    {
        var card = CreateJobCard(title, priority);
        Console.WriteLine("✅ Job card issued by FixIt Pro dispatch.");
        card.PrintSummary();
        return card;
    }
}

// ── Concrete Creators ──────────────────────────────────────
public class PlumbingJobCreator   : JobCardCreator
{
    public override IJobCard CreateJobCard(string title, string priority) =>
        new PlumbingJobCard(title, priority);
}

public class ElectricalJobCreator : JobCardCreator
{
    public override IJobCard CreateJobCard(string title, string priority) =>
        new ElectricalJobCard(title, priority);
}

public class CarpentryJobCreator  : JobCardCreator
{
    public override IJobCard CreateJobCard(string title, string priority) =>
        new CarpentryJobCard(title, priority);
}

// ── Client Code ────────────────────────────────────────────
class Program
{
    static void Main()
    {
        var jobs = new (JobCardCreator creator, string title, string priority)[]
        {
            (new PlumbingJobCreator(),   "Burst pipe in kitchen",      "URGENT"),
            (new ElectricalJobCreator(), "Faulty circuit breaker",     "HIGH"),
            (new CarpentryJobCreator(),  "Broken door frame — Unit 4", "NORMAL"),
        };

        foreach (var (creator, title, priority) in jobs)
            creator.IssueJobCard(title, priority);
    }
}

TypeScript Implementation

// ── Product Interface ──────────────────────────────────────
interface JobCard {
  readonly jobId:    string;
  readonly title:    string;
  readonly category: string;
  readonly priority: string;
  printSummary(): void;
}

// ── Helper ────────────────────────────────────────────────
const shortId = () => Math.random().toString(36).slice(2, 10);

// ── Concrete Products ──────────────────────────────────────
class PlumbingJobCard implements JobCard {
  readonly jobId    = shortId();
  readonly category = "Plumbing";

  constructor(
    readonly title:    string,
    readonly priority: string
  ) {}

  printSummary(): void {
    console.log(`[${this.category}] #${this.jobId} — ${this.title} (Priority: ${this.priority})`);
    console.log("  → Requires: Licensed plumber + water shutoff check");
  }
}

class ElectricalJobCard implements JobCard {
  readonly jobId    = shortId();
  readonly category = "Electrical";

  constructor(
    readonly title:    string,
    readonly priority: string
  ) {}

  printSummary(): void {
    console.log(`[${this.category}] #${this.jobId} — ${this.title} (Priority: ${this.priority})`);
    console.log("  → Requires: Certified electrician + safety inspection");
  }
}

class CarpentryJobCard implements JobCard {
  readonly jobId    = shortId();
  readonly category = "Carpentry";

  constructor(
    readonly title:    string,
    readonly priority: string
  ) {}

  printSummary(): void {
    console.log(`[${this.category}] #${this.jobId} — ${this.title} (Priority: ${this.priority})`);
    console.log("  → Requires: Carpenter + material estimate");
  }
}

// ── Creator ────────────────────────────────────────────────
abstract class JobCardCreator {
  // The Factory Method
  abstract createJobCard(title: string, priority: string): JobCard;

  // Template method — uses the factory method internally
  issueJobCard(title: string, priority: string): JobCard {
    const card = this.createJobCard(title, priority);
    console.log("✅ Job card issued by FixIt Pro dispatch.");
    card.printSummary();
    return card;
  }
}

// ── Concrete Creators ──────────────────────────────────────
class PlumbingJobCreator extends JobCardCreator {
  createJobCard(title: string, priority: string): JobCard {
    return new PlumbingJobCard(title, priority);
  }
}

class ElectricalJobCreator extends JobCardCreator {
  createJobCard(title: string, priority: string): JobCard {
    return new ElectricalJobCard(title, priority);
  }
}

class CarpentryJobCreator extends JobCardCreator {
  createJobCard(title: string, priority: string): JobCard {
    return new CarpentryJobCard(title, priority);
  }
}

// ── Client Code ────────────────────────────────────────────
const jobs: [JobCardCreator, string, string][] = [
  [new PlumbingJobCreator(),   "Burst pipe in kitchen",      "URGENT"],
  [new ElectricalJobCreator(), "Faulty circuit breaker",     "HIGH"  ],
  [new CarpentryJobCreator(),  "Broken door frame — Unit 4", "NORMAL"],
];

for (const [creator, title, priority] of jobs) {
  creator.issueJobCard(title, priority);
}

C# vs TypeScript — Key Differences

Aspect C# TypeScript
Interface declaration public interface IJobCard interface JobCard (no access modifier)
Abstract class abstract class + override keyword abstract class + no override needed
Read-only properties { get; } on auto-properties readonly keyword on fields
Unique ID generation Guid.NewGuid().ToString()[..8] Math.random().toString(36).slice(2, 10)
Tuple destructuring loop foreach var (a, b, c) in tuples for const [a, b, c] of array
Console output Console.WriteLine() console.log()

The structural pattern is identical in both languages. This is the core insight of design patterns — they are language-agnostic. Only the syntax changes.


When to Use the Factory Method

Use it when:

  • You don't know ahead of time which concrete class you need to instantiate
  • You want subclasses or separate classes to control what gets created
  • You're building a framework and want consumers to extend it without modifying existing code
  • Object creation logic is growing into a complex conditional

Avoid it when:

  • You only ever create one type of object — the abstraction isn't worth the overhead
  • A simple function or static method would solve the problem just as well

Real-World Takeaway

The Factory Method is one of the most commonly used patterns in production codebases. You'll find it in ASP.NET's ILoggerFactory, Angular's dependency injection system, and NestJS providers. Anytime you see a method named Create... or Build... that returns an interface type, there's likely a Factory Method underneath.

In FixIt Pro terms: when the dispatch centre receives a new job request, it doesn't need to know how to construct a plumbing card vs an electrical card. It just calls IssueJobCard on the right creator and gets back a valid IJobCard. The complexity is hidden where it belongs.


Source Code

The full code for this series lives on GitHub:
github.com/antonlungameni/fixit-pro-design-patterns

fixit-pro-design-patterns/
├── csharp/Creational/01-FactoryMethod/
└── typescript/Creational/01-FactoryMethod/

Next up: #02 — Abstract Factory Pattern
Build notification kits for different client types — Homeowner vs Business — without coupling to concrete classes.