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
// 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
// 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.