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.