Symptom

Tree-shaped data is represented as untyped records with a discriminating field; every operation walks the tree by switching on that field. Adding a new node kind requires editing every operation; adding a new operation requires re-implementing the switch.

Goal

Leaf and composite share an interface; the composite's implementation of any operation delegates to its children. Client code treats trees and leaves uniformly; new node kinds are new classes; new operations are new methods.

Before the pattern

function totalSize(node) {
if (node.type === 'file') {
return node.bytes;
} else if (node.type === 'directory') {
let total = 0;
for (const child of node.children) {
total += totalSize(child);
}
return total;
}
throw new Error(`unknown node type: ${node.type}`);
}
function nameOf(node) {
if (node.type === 'file' || node.type === 'directory') return node.name;
throw new Error(`unknown node type: ${node.type}`);
}
// Every traversal repeats the if/else; new node types touch every function.

After the pattern

class FileNode {
constructor(name, bytes) {
this.name = name;
this.bytes = bytes;
}
totalSize() { return this.bytes; }
}
class DirectoryNode {
constructor(name, children) {
this.name = name;
this.children = children;
}
totalSize() {
return this.children.reduce((sum, child) => sum + child.totalSize(), 0);
}
}
// Client treats leaf and composite uniformly:
const root = new DirectoryNode('project', [
new FileNode('README.md', 1024),
new DirectoryNode('src', [new FileNode('index.js', 2048)]),
]);
const size = root.totalSize();
Example source: Illustrative example written for this site in the spirit of Design Patterns (Gamma, Helm, Johnson, Vlissides, Addison-Wesley, 1994), chapter 4. The book's running example is a graphics editor's drawing primitives + groups; this JavaScript adaptation uses filesystem nodes (file + directory) because the leaf/composite uniformity is more recognizable and the recursive aggregation is easier to picture.
Pressure

Repeated Switches at every traversal: totalSize switches on type, nameOf switches on type, render switches on type. Each switch grows in lockstep when a new kind ships; one missed switch ships as a runtime error on a code path the test suite may not exercise.

Tradeoff

Leaf and composite must agree on the interface even when an operation makes no sense on one of them — leaves often implement add()/remove() child operations as throws or no-ops, exposing the divergence in shape behind the uniform interface. The pattern is at its sharpest when every operation is meaningful on both sides; it loses some elegance otherwise.

Relief

totalSize() is one expression: 'sum the children'; client code reads as `root.totalSize()` regardless of how deep the tree is. New node kinds land as new classes without editing existing traversals; new operations land as new methods without editing client code.

Trap

When leaf and composite genuinely diverge in their useful operations — composite needs add/remove, leaf does not — the uniform interface forces leaves to expose meaningless operations or throw on them. The transparency-vs-safety tradeoff (uniform-but-unsafe vs typed-but-discriminated) is real; pick deliberately.