Composite Pattern — FixIt Pro Series #08
A Full Kitchen Renovation is made up of plumbing, electrical, and carpentry job cards. Learn how the Composite pattern lets you treat a single job and a group of jobs identically — in C# and TypeScript.
Series: Design Patterns with FixIt Pro · Episode 08 / 22 · Structural Pattern
Previous: #07 — Bridge Pattern
The Scenario
FixIt Pro has a new client — a property developer managing a full kitchen renovation across 10 units. The renovation breaks down like this:
- Full Kitchen Renovation (compound job)
- Replace burst pipe under sink (Plumbing)
- Install new circuit breaker (Electrical)
- Fit new cabinet frames (Carpentry)
- Countertop Installation (another compound job)
- Remove old countertop (Carpentry)
- Waterproof surface (Plumbing)
- Install new countertop (Carpentry)
This is a tree structure. Some nodes are individual jobs. Some nodes are groups of jobs that contain other jobs — including other groups.
The dispatcher needs to calculate the total cost, assign handymen, and mark everything complete. It shouldn't need to know whether it's dealing with a single job or a group. The logic should be the same either way.
That's the Composite pattern.
What Is the Composite Pattern?
Compose objects into tree structures to represent part-whole hierarchies. The Composite lets clients treat individual objects and compositions of objects uniformly.
The key word is uniformly. A single job card and a compound job card that contains 10 nested jobs both respond to the same interface. The client calls Execute() — it doesn't need to know what's underneath.
The three participants
| Role | FixIt Pro equivalent |
|---|---|
| Component interface | IJobComponent — declares operations common to both leaves and composites |
| Leaf | JobCard — a single, indivisible job card |
| Composite | CompoundJob — a job that contains other IJobComponent instances |
C# Implementation
// ── Component Interface ────────────────────────────────────
public interface IJobComponent
{
string Name { get; }
decimal Cost { get; }
void Execute();
void Display(int depth = 0);
}
// ── Leaf ───────────────────────────────────────────────────
public class JobCard : IJobComponent
{
public string Name { get; }
public decimal Cost { get; }
public string Category { get; }
public string Assignee { get; set; } = "Unassigned";
public JobCard(string name, string category, decimal cost)
{
Name = name;
Category = category;
Cost = cost;
}
public void Execute() =>
Console.WriteLine($"{"".PadLeft(4)}✅ Executing: [{Category}] {Name} — N${Cost:N2} | Assigned to: {Assignee}");
public void Display(int depth = 0) =>
Console.WriteLine($"{"".PadLeft(depth * 2)}📋 {Name} [{Category}] — N${Cost:N2}");
}
// ── Composite ──────────────────────────────────────────────
public class CompoundJob : IJobComponent
{
private readonly List<IJobComponent> _children = new();
public string Name { get; }
// Cost is the sum of all children — recursively
public decimal Cost => _children.Sum(c => c.Cost);
public CompoundJob(string name)
{
Name = name;
}
public void Add(IJobComponent component) => _children.Add(component);
public void Remove(IJobComponent component) => _children.Remove(component);
public void Execute()
{
Console.WriteLine($"\n🔧 Starting compound job: {Name} (Total: N${Cost:N2})");
foreach (var child in _children)
child.Execute();
Console.WriteLine($"✔️ Compound job complete: {Name}");
}
public void Display(int depth = 0)
{
Console.WriteLine($"{"".PadLeft(depth * 2)}📁 {Name} [Compound — {_children.Count} tasks, N${Cost:N2}]");
foreach (var child in _children)
child.Display(depth + 1);
}
}
// ── Client Code ────────────────────────────────────────────
class Program
{
static void Main()
{
// ── Build the tree ─────────────────────────────────
// Leaf jobs
var replacePipe = new JobCard("Replace burst pipe under sink", "Plumbing", 850m);
var installBreaker = new JobCard("Install new circuit breaker", "Electrical", 1200m);
var fitCabinets = new JobCard("Fit new cabinet frames", "Carpentry", 2400m);
// Nested compound job
var countertopJob = new CompoundJob("Countertop Installation");
countertopJob.Add(new JobCard("Remove old countertop", "Carpentry", 400m));
countertopJob.Add(new JobCard("Waterproof surface", "Plumbing", 600m));
countertopJob.Add(new JobCard("Install new countertop", "Carpentry", 3200m));
// Root compound job
var kitchenRenovation = new CompoundJob("Full Kitchen Renovation");
kitchenRenovation.Add(replacePipe);
kitchenRenovation.Add(installBreaker);
kitchenRenovation.Add(fitCabinets);
kitchenRenovation.Add(countertopJob); // composite inside composite
// ── Display the tree ───────────────────────────────
Console.WriteLine("=== Job Tree ===");
kitchenRenovation.Display();
// ── Execute everything ─────────────────────────────
Console.WriteLine("\n=== Executing Jobs ===");
kitchenRenovation.Execute();
// ── Uniform treatment ──────────────────────────────
// The client doesn't care whether it's a leaf or composite
Console.WriteLine("\n=== Treating leaf and composite uniformly ===");
IJobComponent[] components = { replacePipe, kitchenRenovation };
foreach (var component in components)
Console.WriteLine($"{component.Name} — Total Cost: N${component.Cost:N2}");
}
}
Output:
=== Job Tree ===
📁 Full Kitchen Renovation [Compound — 4 tasks, N$8,650.00]
📋 Replace burst pipe under sink [Plumbing] — N$850.00
📋 Install new circuit breaker [Electrical] — N$1,200.00
📋 Fit new cabinet frames [Carpentry] — N$2,400.00
📁 Countertop Installation [Compound — 3 tasks, N$4,200.00]
📋 Remove old countertop [Carpentry] — N$400.00
📋 Waterproof surface [Plumbing] — N$600.00
📋 Install new countertop [Carpentry] — N$3,200.00
=== Executing Jobs ===
🔧 Starting compound job: Full Kitchen Renovation (Total: N$8,650.00)
✅ Executing: [Plumbing] Replace burst pipe under sink — N$850.00 | Assigned to: Unassigned
✅ Executing: [Electrical] Install new circuit breaker — N$1,200.00 | Assigned to: Unassigned
✅ Executing: [Carpentry] Fit new cabinet frames — N$2,400.00 | Assigned to: Unassigned
🔧 Starting compound job: Countertop Installation (Total: N$4,200.00)
✅ Executing: [Carpentry] Remove old countertop — N$400.00 | Assigned to: Unassigned
✅ Executing: [Plumbing] Waterproof surface — N$600.00 | Assigned to: Unassigned
✅ Executing: [Carpentry] Install new countertop — N$3,200.00 | Assigned to: Unassigned
✔️ Compound job complete: Countertop Installation
✔️ Compound job complete: Full Kitchen Renovation
=== Treating leaf and composite uniformly ===
Replace burst pipe under sink — Total Cost: N$850.00
Full Kitchen Renovation — Total Cost: N$8,650.00
TypeScript Implementation
// ── Component/IJobComponent.ts ─────────────────────────────
export interface IJobComponent {
readonly name: string;
readonly cost: number;
execute(): void;
display(depth?: number): void;
}
// ── Leaf/JobCard.ts ────────────────────────────────────────
import { IJobComponent } from "../Component/IJobComponent";
export class JobCard implements IJobComponent {
assignee: string = "Unassigned";
constructor(
readonly name: string,
readonly category: string,
readonly cost: number
) {}
execute(): void {
console.log(
`${" ".repeat(2)}✅ Executing: [${this.category}] ${this.name} — N$${this.cost.toFixed(2)} | Assigned to: ${this.assignee}`
);
}
display(depth: number = 0): void {
console.log(
`${" ".repeat(depth)}📋 ${this.name} [${this.category}] — N$${this.cost.toFixed(2)}`
);
}
}
// ── Composite/CompoundJob.ts ───────────────────────────────
import { IJobComponent } from "../Component/IJobComponent";
export class CompoundJob implements IJobComponent {
private children: IJobComponent[] = [];
constructor(readonly name: string) {}
// Cost is the sum of all children — recursively
get cost(): number {
return this.children.reduce((sum, child) => sum + child.cost, 0);
}
add(component: IJobComponent): void { this.children.push(component); }
remove(component: IJobComponent): void {
this.children = this.children.filter(c => c !== component);
}
execute(): void {
console.log(`\n🔧 Starting compound job: ${this.name} (Total: N$${this.cost.toFixed(2)})`);
for (const child of this.children)
child.execute();
console.log(`✔️ Compound job complete: ${this.name}`);
}
display(depth: number = 0): void {
console.log(
`${" ".repeat(depth)}📁 ${this.name} [Compound — ${this.children.length} tasks, N$${this.cost.toFixed(2)}]`
);
for (const child of this.children)
child.display(depth + 1);
}
}
// ── App.ts ─────────────────────────────────────────────────
import { JobCard } from "./Leaf/JobCard";
import { CompoundJob } from "./Composite/CompoundJob";
import { IJobComponent } from "./Component/IJobComponent";
// ── Build the tree ─────────────────────────────────────────
const replacePipe = new JobCard("Replace burst pipe under sink", "Plumbing", 850);
const installBreaker = new JobCard("Install new circuit breaker", "Electrical", 1200);
const fitCabinets = new JobCard("Fit new cabinet frames", "Carpentry", 2400);
const countertopJob = new CompoundJob("Countertop Installation");
countertopJob.add(new JobCard("Remove old countertop", "Carpentry", 400));
countertopJob.add(new JobCard("Waterproof surface", "Plumbing", 600));
countertopJob.add(new JobCard("Install new countertop", "Carpentry", 3200));
const kitchenRenovation = new CompoundJob("Full Kitchen Renovation");
kitchenRenovation.add(replacePipe);
kitchenRenovation.add(installBreaker);
kitchenRenovation.add(fitCabinets);
kitchenRenovation.add(countertopJob);
// ── Display the tree ───────────────────────────────────────
console.log("=== Job Tree ===");
kitchenRenovation.display();
// ── Execute everything ─────────────────────────────────────
console.log("\n=== Executing Jobs ===");
kitchenRenovation.execute();
// ── Uniform treatment ──────────────────────────────────────
console.log("\n=== Treating leaf and composite uniformly ===");
const components: IJobComponent[] = [replacePipe, kitchenRenovation];
for (const component of components)
console.log(`${component.name} — Total Cost: N$${component.cost.toFixed(2)}`);
C# vs TypeScript — Key Differences
| Aspect | C# | TypeScript |
|---|---|---|
| Recursive cost | _children.Sum(c => c.Cost) |
children.reduce((sum, c) => sum + c.cost, 0) |
| Remove from list | _children.Remove(component) |
children.filter(c => c !== component) |
| Computed property | public decimal Cost => ... |
get cost(): number { return ...; } |
| String padding | "".PadLeft(depth * 2) |
" ".repeat(depth) |
| Currency format | N${Cost:N2} format specifier |
cost.toFixed(2) |
| Decimal type | decimal — high precision, financial safe |
number — IEEE 754 double; use a library like decimal.js for production financials |
The last row is important. In production financial code, never use number in TypeScript for currency. JavaScript's floating-point arithmetic will bite you: 0.1 + 0.2 === 0.30000000000000004. Use decimal.js or big.js, or work in integer cents.
When to Use the Composite
Use it when:
- You need to represent part-whole hierarchies (trees of objects)
- You want clients to treat individual objects and compositions uniformly
- The structure can be nested arbitrarily deep
Avoid it when:
- Your hierarchy has a fixed, known depth — a simpler data structure may suffice
- The uniformity constraint forces an overly generic interface that doesn't fit all leaf types cleanly
Real-World Takeaway
The Composite is the pattern behind file systems — a File and a Folder both implement the same interface, and a Folder can contain other Folders. It's behind UI component trees in React, Angular, and every major frontend framework — a <Button> and a <Form> (which contains many components) both implement the same render interface. It's behind ASP.NET's middleware pipeline and HTML's DOM.
Any time you have a tree where nodes and leaves need to behave the same way, you're looking at a Composite.
In FixIt Pro, the property developer gets one Execute() call on the root CompoundJob. Every nested job — however deep the tree goes — runs automatically. The total cost is always accurate, always recursive. The dispatcher never loops manually over nested structures. It just calls Execute().
Repo Structure for This Episode
github.com/antonlungameni/fixit-pro-design-patterns
fixit-pro-design-patterns/
├── csharp/Structural/08-Composite/
│ ├── Component/IJobComponent.cs
│ ├── Leaf/JobCard.cs
│ ├── Composite/CompoundJob.cs
│ └── Program.cs
└── typescript/Structural/08-Composite/
├── Component/IJobComponent.ts
├── Leaf/JobCard.ts
├── Composite/CompoundJob.ts
└── App.ts
Previous: #07 — Bridge Pattern
Next up: #09 — Decorator Pattern
Adding extras to a job card at runtime — UrgentJobCard, InsuredJobCard, WarrantiedJobCard — without modifying the original class or creating a subclass for every combination.