Composite Pattern
Compose objects into tree structures to represent part-whole hierarchies, letting clients treat individual objects and compositions uniformly.
Overview
Real software rarely deals with flat lists of objects. Code editors manage files inside folders inside projects. UIs nest buttons inside panels inside windows. Bills of materials list parts that are themselves assemblies of smaller parts. In all of these domains, you want to walk the tree and do something — calculate total size, render to screen, compute cost — without caring whether you are standing at a leaf or at a subtree.
The Composite pattern is a structural design pattern that lets you compose objects into tree structures and then work with those trees as if each element were the same kind of thing. A file and a directory both respond to size(). A button and a panel both respond to render(). Client code only ever talks to one interface, regardless of depth.
Understanding Composite requires seeing it as two cooperating ideas: a shared abstraction that unifies leaves and composites, and a recursive structure where composites hold lists of the same abstraction.
Problem & Motivation
Suppose you are building a file-system utility that reports the total disk usage of any path. A file has a known byte size. A directory has no size of its own — its size is the sum of everything inside it, recursively.
Without a unifying abstraction, the calling code has to branch constantly:
if entry is a file → add its size
if entry is a directory → iterate its children and repeatEvery new traversal — search, delete, permission check — must replicate this branching logic. You also need to handle arbitrary nesting depth, which pushes you toward recursion anyway. The result is fragile code that knows too much about the tree's internal structure.
The Composite pattern cuts through this by giving files and directories the same interface. The directory's size() implementation just calls size() on each child and sums the results. The caller never needs to know what it is holding.
Class Diagram
Loading diagram...
The key structural points are:
FileSystemEntryis the component interface — it declares operations that both leaves and composites must implement.Fileis a leaf — it has no children and implements all operations directly.Directoryis the composite — it holds a list ofFileSystemEntrychildren and delegates operations to them recursively.- Client code depends only on
FileSystemEntry, so it works identically with a single file or a deeply nested directory tree.
Implementation
The implementations below model a file system where File is a leaf and Directory is a composite. Calling display() on the root prints the entire tree; calling size() returns the total bytes recursively. All four versions produce the same observable output.
interface FileSystemEntry {
name(): string;
size(): number;
display(indent?: number): void;
}
class File implements FileSystemEntry {
constructor(
private readonly _name: string,
private readonly _size: number,
) {}
name(): string {
return this._name;
}
size(): number {
return this._size;
}
display(indent = 0): void {
console.log(`${" ".repeat(indent)}- ${this._name} (${this._size} B)`);
}
}
class Directory implements FileSystemEntry {
private children: FileSystemEntry[] = [];
constructor(private readonly _name: string) {}
name(): string {
return this._name;
}
add(entry: FileSystemEntry): void {
this.children.push(entry);
}
remove(entry: FileSystemEntry): void {
this.children = this.children.filter((c) => c !== entry);
}
size(): number {
return this.children.reduce((total, child) => total + child.size(), 0);
}
display(indent = 0): void {
console.log(`${" ".repeat(indent)}+ ${this._name}/`);
for (const child of this.children) {
child.display(indent + 1);
}
}
}
// Build tree
const root = new Directory("project");
const src = new Directory("src");
src.add(new File("index.ts", 1200));
src.add(new File("utils.ts", 800));
const assets = new Directory("assets");
assets.add(new File("logo.png", 45000));
root.add(src);
root.add(assets);
root.add(new File("package.json", 600));
root.display();
// + project/
// + src/
// - index.ts (1200 B)
// - utils.ts (800 B)
// + assets/
// - logo.png (45000 B)
// - package.json (600 B)
console.log("Total size:", root.size(), "B"); // Total size: 47600 BThe Uniformity Principle
The pattern's power comes entirely from treating leaves and composites through the same interface. If you find yourself writing if entry instanceof Directory anywhere in client code, the abstraction has leaked. Move that branching into the composite's own methods, where it belongs. Client code should never need to know whether it holds a leaf or a subtree.
Real-World Examples
GUI widget trees — Every major UI toolkit (Swing, Qt, Flutter, the browser DOM) is a Composite. A Widget or Node interface defines render(), layout(), and event handling. Buttons, text inputs, and images are leaves. Panels, scrollable containers, and windows are composites. The rendering engine calls render() on the root and the entire tree paints itself through recursion — without the engine ever distinguishing leaf from composite.
Build systems and task graphs — Tools like Gradle, Make, and Bazel model builds as trees of tasks. A leaf task runs a single shell command. A composite task aggregates child tasks that must run first. Calling build() on the root executes the entire dependency graph. The build runner does not care whether a task is atomic or a subtree — it just calls execute() on whatever it holds.
Expression trees in compilers and spreadsheets — Mathematical expressions form natural trees. The expression (a + b) * (c - 3) is a multiply node whose children are an add node (leaves a and b) and a subtract node (leaf c and literal 3). Evaluating the tree, formatting it as a string, or optimizing it all recurse through the same Expression interface. Spreadsheet formula engines, SQL query planners, and interpreter ASTs all use this structure.
Pros and Cons
Advantages
Uniform client code — operations like size, render, and search are written once against the component interface and work at any level of the tree without modification.
Open for extension — adding a new node type (e.g., a SymLink that delegates to another entry) requires only a new class implementing the component interface. Nothing else changes.
Natural recursion — tree traversal is expressed in the composite's own methods, not scattered across callers. The recursive structure mirrors the recursive data.
Simplifies complex hierarchies — deeply nested structures (DOM, scene graphs, org charts) become manageable because every node speaks the same language.
Disadvantages
Overly general interface — if leaves and composites have very different behaviors, forcing them through a single interface can lead to dummy implementations or exceptions thrown from leaf nodes when composite-only methods are called.
Child management placement is awkward — methods like add() and remove() belong only on composites. Putting them on the component interface violates the principle of least surprise for leaf users; keeping them off the interface requires casting, which breaks uniformity.
Performance with large trees — naive recursive traversal of very wide or very deep trees can be slow. Caching aggregated values (e.g., memoizing size()) adds complexity.
Circular reference risk — if code mistakenly adds an ancestor as a child, traversal loops forever. The pattern provides no built-in cycle detection.
When to Use / When to Avoid
Use Composite when:
- Your domain naturally forms a part-whole hierarchy — file systems, UI trees, org charts, bill-of-materials, expression trees.
- You want client code to treat individual objects and groups of objects identically.
- You need to add new node types without modifying existing traversal code.
- The tree structure is determined at runtime, not compile time.
Avoid Composite when:
- The objects in your system do not actually form a hierarchy — applying Composite to a flat collection adds indirection with no benefit.
- Leaf and composite behaviors are so different that a shared interface becomes a thin, misleading abstraction.
- You need strong type safety distinguishing leaves from composites — a typed tree with separate interfaces is clearer than a uniform one with dummy methods.
- Performance is critical and memoization or lazy evaluation would make the composite's recursive methods complex enough to obscure the pattern's intent.
Related Patterns
Decorator — also wraps a component interface, but adds behavior to a single object rather than aggregating a collection of children. Composite builds trees; Decorator builds chains. They are often used together: a Decorator can wrap a Composite node to add logging or caching without touching the tree structure.
Iterator — provides a way to traverse a Composite tree without exposing its internal structure. An Iterator that walks a tree depth-first or breadth-first is a natural companion to Composite, especially when the traversal order matters to the caller.
Visitor — lets you add new operations to a Composite tree without modifying the node classes. Where Composite controls how recursion flows, Visitor separates the operation logic from the tree structure. Use Visitor when you expect many new operations on a stable tree shape.
Flyweight — reduces memory cost in Composite trees that have many identical leaf nodes. If a large document contains thousands of character nodes with the same font and size, Flyweight shares the immutable state across them. The Composite structure provides the tree; Flyweight keeps it memory-efficient.
Design Tip
The hardest decision in Composite is where to put child-management methods (add, remove). Placing them on the component interface maximizes transparency — callers never need to cast — but means leaf implementations must throw or do nothing for methods that make no sense on them. Placing them only on the composite class preserves type safety but requires clients to check or cast when they need to modify structure. There is no universally correct answer. Prefer the component-level approach when most callers build the tree; prefer the composite-level approach when most callers only read it.