Back to Blog
design-patternscsharptypescriptstructural-patterns

Flyweight Pattern — FixIt Pro Series #11

FixIt Pro manages thousands of job cards. Learn how the Flyweight pattern shares common metadata across instances to drastically reduce memory usage in C# and TypeScript.


Series: Design Patterns with FixIt Pro  ·  Episode 11 / 22  ·  Structural Pattern
Previous: #10 — Facade Pattern


The Scenario

FixIt Pro has scaled. The platform now processes thousands of job cards every day across hundreds of properties. Each job card carries:

  • A unique job ID
  • A client reference
  • A scheduled date
  • A status
  • Category metadata — tool requirements, safety checklists, material specs, regulatory notes

That last item is the problem. The category metadata for "Plumbing" is identical across every plumbing job card. "Electrical" metadata is identical across every electrical job card. Yet every JobCard object in memory carries its own copy of that data.

With 10,000 active job cards and 3 categories, you're storing the same metadata strings 10,000 times instead of 3.

The Flyweight pattern fixes this by sharing the repeated data.


What Is the Flyweight Pattern?

Use sharing to efficiently support a large number of fine-grained objects.

The Flyweight separates an object's state into two parts:

  • Intrinsic state — data that is shared, context-independent, and stored inside the flyweight
  • Extrinsic state — data that is unique per instance, context-dependent, and passed in by the caller

In FixIt Pro terms:

State type Data
Intrinsic (shared) Category name, tool requirements, safety checklist, material specs, regulatory notes
Extrinsic (unique) Job ID, client reference, scheduled date, status

The three participants

Role FixIt Pro equivalent
Flyweight JobCategoryMetadata — the shared, immutable intrinsic state
Flyweight Factory JobCategoryFactory — creates and caches flyweight instances
Context JobCard — holds extrinsic state, references a flyweight

C# Implementation

// ── Flyweight — Intrinsic State ────────────────────────────
// Immutable. Shared across all job cards of the same category.

public sealed class JobCategoryMetadata
{
    public string   Category         { get; }
    public string[] ToolRequirements { get; }
    public string[] SafetyChecklist  { get; }
    public string   RegulatoryNote   { get; }

    public JobCategoryMetadata(
        string   category,
        string[] toolRequirements,
        string[] safetyChecklist,
        string   regulatoryNote)
    {
        Category         = category;
        ToolRequirements = toolRequirements;
        SafetyChecklist  = safetyChecklist;
        RegulatoryNote   = regulatoryNote;
    }

    public void PrintMetadata()
    {
        Console.WriteLine($"  Category  : {Category}");
        Console.WriteLine($"  Tools     : {string.Join(", ", ToolRequirements)}");
        Console.WriteLine($"  Safety    : {string.Join(", ", SafetyChecklist)}");
        Console.WriteLine($"  Regulatory: {RegulatoryNote}");
    }
}

// ── Flyweight Factory ──────────────────────────────────────
// Creates flyweights on first request, returns cached copy thereafter.

public static class JobCategoryFactory
{
    private static readonly Dictionary<string, JobCategoryMetadata> _cache = new();

    public static JobCategoryMetadata GetMetadata(string category)
    {
        if (_cache.TryGetValue(category, out var cached))
        {
            Console.WriteLine($"  [Factory] Returning cached metadata for '{category}'.");
            return cached;
        }

        Console.WriteLine($"  [Factory] Creating new metadata for '{category}'.");

        var metadata = category switch
        {
            "Plumbing" => new JobCategoryMetadata(
                "Plumbing",
                new[] { "pipe wrench", "plunger", "sealing tape", "pressure gauge" },
                new[] { "shut off water main", "check pressure relief valve" },
                "Must comply with SANS 10400-W water installation standards."),

            "Electrical" => new JobCategoryMetadata(
                "Electrical",
                new[] { "multimeter", "wire stripper", "circuit tester", "insulated screwdrivers" },
                new[] { "isolate circuit at DB board", "test for live wires before work" },
                "Work must be performed by a registered electrician per NEC regulations."),

            "Carpentry" => new JobCategoryMetadata(
                "Carpentry",
                new[] { "circular saw", "drill", "spirit level", "tape measure" },
                new[] { "wear safety goggles", "check for hidden cables before cutting" },
                "Structural work requires approved building plans."),

            _ => new JobCategoryMetadata(
                category,
                new[] { "standard toolkit" },
                new[] { "follow site safety protocol" },
                "No specific regulatory requirement.")
        };

        _cache[category] = metadata;
        return metadata;
    }

    public static int CacheSize => _cache.Count;
}

// ── Context — Extrinsic State ──────────────────────────────
// Each JobCard holds its own unique data but SHARES the metadata flyweight.

public class JobCard
{
    // Extrinsic state — unique per card
    public string   JobId      { get; } = Guid.NewGuid().ToString()[..8];
    public string   ClientRef  { get; }
    public DateTime ScheduledAt { get; }
    public string   Status     { get; set; } = "Active";

    // Intrinsic state — shared flyweight reference
    private readonly JobCategoryMetadata _metadata;

    public JobCard(string clientRef, DateTime scheduledAt, string category)
    {
        ClientRef   = clientRef;
        ScheduledAt = scheduledAt;
        _metadata   = JobCategoryFactory.GetMetadata(category);
    }

    public void PrintSummary()
    {
        Console.WriteLine($"\n  Job #{JobId} | Client: {ClientRef} | {ScheduledAt:dd MMM yyyy} | {Status}");
        _metadata.PrintMetadata();
    }
}

// ── Client Code ────────────────────────────────────────────

class Program
{
    static void Main()
    {
        Console.WriteLine("=== Creating 6 job cards across 3 categories ===\n");

        var jobs = new[]
        {
            new JobCard("Sarah Ndapewa",  new DateTime(2025, 6, 2),  "Plumbing"),
            new JobCard("John Shipanga",  new DateTime(2025, 6, 3),  "Electrical"),
            new JobCard("Maria Haufiku", new DateTime(2025, 6, 4),  "Carpentry"),
            new JobCard("Peter Nangolo", new DateTime(2025, 6, 5),  "Plumbing"),
            new JobCard("Anna Nghoshi",  new DateTime(2025, 6, 6),  "Electrical"),
            new JobCard("Tom Kamati",    new DateTime(2025, 6, 7),  "Plumbing"),
        };

        Console.WriteLine($"\nMetadata objects in cache: {JobCategoryFactory.CacheSize}");
        Console.WriteLine($"Job cards created        : {jobs.Length}");
        Console.WriteLine("(3 shared metadata objects serve 6 job cards)\n");

        Console.WriteLine("=== Job Summaries ===");
        foreach (var job in jobs)
            job.PrintSummary();

        // Verify sharing — same category = same metadata object
        var job1 = jobs[0]; // Plumbing
        var job4 = jobs[3]; // Plumbing

        // Access metadata via reflection for demonstration
        var field    = typeof(JobCard).GetField("_metadata",
                           System.Reflection.BindingFlags.NonPublic |
                           System.Reflection.BindingFlags.Instance)!;

        var meta1 = field.GetValue(job1);
        var meta4 = field.GetValue(job4);

        Console.WriteLine($"\nJob1 and Job4 share same metadata object: {ReferenceEquals(meta1, meta4)}");
        // Output: True
    }
}

Output:

=== Creating 6 job cards across 3 categories ===

  [Factory] Creating new metadata for 'Plumbing'.
  [Factory] Creating new metadata for 'Electrical'.
  [Factory] Creating new metadata for 'Carpentry'.
  [Factory] Returning cached metadata for 'Plumbing'.
  [Factory] Returning cached metadata for 'Electrical'.
  [Factory] Returning cached metadata for 'Plumbing'.

Metadata objects in cache: 3
Job cards created        : 6
(3 shared metadata objects serve 6 job cards)

Job1 and Job4 share same metadata object: True

TypeScript Implementation

// ── Flyweight/JobCategoryMetadata.ts ──────────────────────

export class JobCategoryMetadata {
  constructor(
    readonly category:         string,
    readonly toolRequirements: string[],
    readonly safetyChecklist:  string[],
    readonly regulatoryNote:   string
  ) {}

  printMetadata(): void {
    console.log(`  Category  : ${this.category}`);
    console.log(`  Tools     : ${this.toolRequirements.join(", ")}`);
    console.log(`  Safety    : ${this.safetyChecklist.join(", ")}`);
    console.log(`  Regulatory: ${this.regulatoryNote}`);
  }
}

// ── Flyweight/JobCategoryFactory.ts ───────────────────────

import { JobCategoryMetadata } from "./JobCategoryMetadata";

const definitions: Record<string, Omit<ConstructorParameters<typeof JobCategoryMetadata>[0], never>> = {};

type MetadataArgs = ConstructorParameters<typeof JobCategoryMetadata>;

const categoryDefinitions: Record<string, MetadataArgs> = {
  Plumbing: [
    "Plumbing",
    ["pipe wrench", "plunger", "sealing tape", "pressure gauge"],
    ["shut off water main", "check pressure relief valve"],
    "Must comply with SANS 10400-W water installation standards.",
  ],
  Electrical: [
    "Electrical",
    ["multimeter", "wire stripper", "circuit tester", "insulated screwdrivers"],
    ["isolate circuit at DB board", "test for live wires before work"],
    "Work must be performed by a registered electrician per NEC regulations.",
  ],
  Carpentry: [
    "Carpentry",
    ["circular saw", "drill", "spirit level", "tape measure"],
    ["wear safety goggles", "check for hidden cables before cutting"],
    "Structural work requires approved building plans.",
  ],
};

export class JobCategoryFactory {
  private static cache = new Map<string, JobCategoryMetadata>();

  static getMetadata(category: string): JobCategoryMetadata {
    if (JobCategoryFactory.cache.has(category)) {
      console.log(`  [Factory] Returning cached metadata for '${category}'.`);
      return JobCategoryFactory.cache.get(category)!;
    }

    console.log(`  [Factory] Creating new metadata for '${category}'.`);

    const args = categoryDefinitions[category] ?? [
      category,
      ["standard toolkit"],
      ["follow site safety protocol"],
      "No specific regulatory requirement.",
    ];

    const metadata = new JobCategoryMetadata(...args);
    JobCategoryFactory.cache.set(category, metadata);
    return metadata;
  }

  static get cacheSize(): number {
    return JobCategoryFactory.cache.size;
  }
}

// ── Context/JobCard.ts ─────────────────────────────────────

import { JobCategoryMetadata } from "../Flyweight/JobCategoryMetadata";
import { JobCategoryFactory  } from "../Flyweight/JobCategoryFactory";

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

export class JobCard {
  // Extrinsic state — unique per card
  readonly jobId:       string = shortId();
  status:               string = "Active";

  // Intrinsic state — shared flyweight reference
  private readonly metadata: JobCategoryMetadata;

  constructor(
    readonly clientRef:   string,
    readonly scheduledAt: Date,
    category:             string
  ) {
    this.metadata = JobCategoryFactory.getMetadata(category);
  }

  printSummary(): void {
    console.log(
      `\n  Job #${this.jobId} | Client: ${this.clientRef} | ${this.scheduledAt.toDateString()} | ${this.status}`
    );
    this.metadata.printMetadata();
  }

  // Expose metadata reference for sharing verification
  getMetadata(): JobCategoryMetadata { return this.metadata; }
}

// ── App.ts ─────────────────────────────────────────────────

import { JobCard            } from "./Context/JobCard";
import { JobCategoryFactory } from "./Flyweight/JobCategoryFactory";

console.log("=== Creating 6 job cards across 3 categories ===\n");

const jobs = [
  new JobCard("Sarah Ndapewa",  new Date("2025-06-02"), "Plumbing"),
  new JobCard("John Shipanga",  new Date("2025-06-03"), "Electrical"),
  new JobCard("Maria Haufiku", new Date("2025-06-04"), "Carpentry"),
  new JobCard("Peter Nangolo", new Date("2025-06-05"), "Plumbing"),
  new JobCard("Anna Nghoshi",  new Date("2025-06-06"), "Electrical"),
  new JobCard("Tom Kamati",    new Date("2025-06-07"), "Plumbing"),
];

console.log(`\nMetadata objects in cache: ${JobCategoryFactory.cacheSize}`);
console.log(`Job cards created        : ${jobs.length}`);
console.log("(3 shared metadata objects serve 6 job cards)\n");

console.log("=== Job Summaries ===");
for (const job of jobs) job.printSummary();

// Verify sharing
const meta1 = jobs[0].getMetadata(); // Plumbing
const meta4 = jobs[3].getMetadata(); // Plumbing

console.log(`\nJob1 and Job4 share same metadata object: ${meta1 === meta4}`);
// Output: true

C# vs TypeScript — Key Differences

Aspect C# TypeScript
Immutable class sealed class + readonly properties readonly constructor params
Factory cache Dictionary<string, T> Map<string, T>
Switch for creation category switch { ... } expression Lookup Record<string, args>
Reference equality ReferenceEquals(a, b) a === b
Verify sharing Reflection to access private field Public getMetadata() accessor
Static factory public static class JobCategoryFactory export class with static methods

The Memory Maths

This is where the Flyweight earns its name. Consider FixIt Pro at scale:

Scenario Objects in memory
10,000 job cards, no flyweight 10,000 metadata objects
10,000 job cards, with flyweight 3 metadata objects
100,000 job cards, no flyweight 100,000 metadata objects
100,000 job cards, with flyweight 3 metadata objects

The metadata objects stay constant regardless of how many job cards exist. Only the extrinsic state — job ID, client, date, status — scales with volume. That's the pattern's core promise.


When to Use the Flyweight

Use it when:

  • Your application creates a very large number of objects
  • Most object state can be made extrinsic (passed in rather than stored)
  • Object identity doesn't matter — shared instances are interchangeable
  • Memory usage is a measurable concern in your application

Avoid it when:

  • The number of objects is small — the factory overhead isn't justified
  • Objects need to be mutable — shared state means mutations affect all users of the flyweight
  • The distinction between intrinsic and extrinsic state is unclear or forced

Real-World Takeaway

The Flyweight is the pattern behind character rendering in text editors — a font glyph object is a flyweight shared by every character that uses it; only the position on screen is extrinsic. In game engines, it's behind terrain tiles, particle systems, and sprite sheets. In Java, String.intern() and the integer cache (Integer.valueOf(-128 to 127)) are Flyweight implementations baked into the runtime. In .NET, string interning (string.Intern()) works the same way.

In FixIt Pro, every Plumbing job card in the system holds a reference to the same JobCategoryMetadata object. The safety checklist, tool requirements, and regulatory note are never duplicated — no matter how many plumbing jobs are created. At 100,000 job cards, the metadata footprint is still just three objects.


Repo Structure for This Episode

github.com/antonlungameni/fixit-pro-design-patterns

fixit-pro-design-patterns/
├── csharp/Structural/11-Flyweight/
│   ├── Flyweight/JobCategoryMetadata.cs
│   ├── Flyweight/JobCategoryFactory.cs
│   ├── Context/JobCard.cs
│   └── Program.cs
└── typescript/Structural/11-Flyweight/
    ├── Flyweight/JobCategoryMetadata.ts
    ├── Flyweight/JobCategoryFactory.ts
    ├── Context/JobCard.ts
    └── App.ts

Previous: #10 — Facade Pattern
Next up: #12 — Proxy Pattern
A JobCardProxy that checks a handyman's certification before granting access to electrical job cards — and wraps the registry with access control, logging, and lazy loading without changing the original class.