Structural Pattern Pattern Deep Dive

Composite

Some data is naturally tree-shaped: a folder contains files and other folders, a UI panel contains buttons and other panels. The Composite pattern answers a simple but persistent annoyance: how do you let client code treat a single object and a whole group of objects identically, without the group case leaking type-checks into every caller?

⚠️ The Problem

Model a file system with a File class and a Folder class, and you'll want to answer questions like "how big is this?" for both. But a File's size is just a number it stores, while a Folder's size is the sum of everything inside it — which might include more folders, recursively. Without a shared way to ask both the same question, client code ends up littered with checks like: if (isFolder(item)) { total += sumFolderRecursively(item) } else { total += item.size } — repeated at every call site that ever needs to walk the tree, and duplicated again for every other operation (counting files, searching by name, printing a listing).

✅ The Solution

Define one Component interface that both leaf objects (File) and composite objects (Folder, which holds a list of child Components) implement — e.g. both expose getSize(). A File's getSize() just returns its stored size; a Folder's getSize() sums getSize() across its children, calling itself recursively wherever a child happens to be another Folder. Client code calls getSize() on whatever it has — file or folder — with zero type-checks, because both sides of the tree honor the same contract.

Example 1 — a file system: File and Folder

Primary Example — JavaScript
// The shared Component interface: both leaves and composites expose getSize() and print().
class File {
  constructor(name, sizeKb) {
    this.name = name;
    this.sizeKb = sizeKb;
  }
  getSize() {
    return this.sizeKb;
  }
  print(indent = "") {
    console.log(`${indent}${this.name} (${this.sizeKb} KB)`);
  }
}

class Folder {
  constructor(name) {
    this.name = name;
    this.children = []; // Files or other Folders — either implements the same interface.
  }
  add(component) {
    this.children.push(component);
  }
  getSize() {
    return this.children.reduce((total, child) => total + child.getSize(), 0);
  }
  print(indent = "") {
    console.log(`${indent}${this.name}/ (${this.getSize()} KB)`);
    for (const child of this.children) {
      child.print(indent + "  ");
    }
  }
}

const root = new Folder("project");
const src = new Folder("src");
src.add(new File("index.js", 4));
src.add(new File("utils.js", 2));
root.add(src);
root.add(new File("README.md", 1));

// Client code calls getSize()/print() the same way on a File or a Folder — no type-checks.
root.print();
console.log("Total size:", root.getSize(), "KB");

Nothing in print() or the code that calls root.getSize() ever asks "is this a file or a folder?" — every node in the tree, leaf or branch, answers getSize() the same way, and Folder handles the recursion internally.

Example 2 — a UI component tree

Secondary Example — TypeScript
// Shared Component interface for the UI tree.
interface UIComponent {
  render(indent: string): void;
}

// Leaf: a single, non-container control.
class Button implements UIComponent {
  constructor(private label: string) {}
  render(indent: string): void {
    console.log(`${indent}<Button> ${this.label}`);
  }
}

// Composite: a container that holds other UIComponents, leaf or composite.
class Panel implements UIComponent {
  private children: UIComponent[] = [];

  constructor(private name: string) {}

  add(child: UIComponent): void {
    this.children.push(child);
  }

  render(indent: string): void {
    console.log(`${indent}<Panel name="${this.name}">`);
    for (const child of this.children) {
      child.render(indent + "  ");
    }
    console.log(`${indent}</Panel>`);
  }
}

const toolbar = new Panel("toolbar");
toolbar.add(new Button("Save"));
toolbar.add(new Button("Open"));

const sidebar = new Panel("sidebar");
sidebar.add(new Button("Home"));
sidebar.add(toolbar); // a Panel can contain another Panel — composites nest freely.

sidebar.render(""); // client code just calls render() — never checks Button vs Panel

A Panel can contain Buttons or other Panels interchangeably, and whatever renders the tree calls render() uniformly — the exact same shape of solution as the file system, in a different domain and language.

👍 When To Use It

  • Your data is naturally a tree — file systems, UI component trees, organization charts, nested menus — and you need to run the same operation over individual nodes and whole subtrees alike.
  • Client code is currently full of "is this a group or a single item" checks that repeat at every call site, for every operation.
  • You want new leaf or composite types to plug into existing tree-processing code with zero changes to that code, as long as they implement the shared interface.

👎 When NOT To Use It

  • When the data genuinely isn't hierarchical — forcing a flat list or a single pair of related objects into a tree-shaped interface just adds ceremony.
  • When leaves and composites need meaningfully different operations that don't make sense on both — a shared interface that half its members can't honestly implement (throwing "not supported" for some methods) is a sign Composite doesn't fit.
  • When you need strict, compile-time guarantees about what can contain what — Composite's uniform interface makes it easy to accidentally add a component to itself or create cycles unless you add extra validation.

❓ FAQ

Where does the recursion actually happen?

Entirely inside the composite class (Folder, Panel) — never in client code. Each composite's method calls the same method on each of its children, and if a child happens to be another composite, that call recurses again automatically. The client just calls the method once, at the root.

What happens if a leaf class is asked to do something only composites can do, like add a child?

Depends on how strict you want the interface. Some implementations give every Component an add() method and have leaves throw or no-op if called; others keep add() only on the composite type and accept that client code sometimes needs a type check specifically for mutation operations (not for the shared "process this" operations, which is where Composite earns its keep).

Is Composite the same as a generic tree data structure?

Related, but Composite specifically means "leaves and containers share one interface so callers don't distinguish them." A tree data structure by itself doesn't imply that — you could build a tree where the node and container types have completely different APIs. Composite is the design decision to make them the same.

🙋 There Are No Dumb Questions

Q: Doesn't calling getSize() on a huge folder tree get slow if it recalculates every time?
A: It can, and that's a real tradeoff — Composite as shown here recomputes on every call. If that becomes a bottleneck, composites commonly cache the computed value and invalidate the cache when a child is added or removed, which keeps the uniform interface intact while avoiding repeated full-tree walks.

✅ Quick Check

What does the Composite pattern let client code do?


Next up: what if you want to add optional behavior — like milk or sugar on a coffee order — without subclassing for every possible combination? That's the Decorator pattern.

← Back to
Next →