Back to Blog
design-patternscsharptypescriptstructural-patterns

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.