Composite Pattern
Treat individual objects and compositions uniformly. The recursive tree-of-things pattern that shows up everywhere.
What it is#
The Composite pattern composes objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly. A folder contains files and other folders; a panel contains buttons and other panels; an expression contains numbers and other expressions. In each case, the “container” is a “thing” — and that single insight is what the pattern names.
The pattern has three roles. The Component is the interface (or abstract class) shared by both individual objects and containers. The Leaf is a Component with no children — a single file, a single button, a literal number. The Composite is a Component that holds other Components as children — a folder, a panel, an Add(left, right) expression. The Composite forwards operations to its children, often combining their results.
The crucial property is uniform treatment. Client code does not branch on if (node instanceof Leaf). It calls node.getSize(), and the leaf returns its own size while the composite recurses into its children and sums them. The recursion lives inside the composite, not in the caller.
Class structure#
┌────────────────────────────┐ │ Component │ ├────────────────────────────┤ │ operation() │ │ add(Component) [opt] │ │ remove(Component)[opt] │ └─────────▲──────────────────┘ │ ┌─────────────┴────────────┐ │ │ ┌──────┴──────┐ ┌─────────┴─────────┐ │ Leaf │ │ Composite │◇──┐ ├─────────────┤ ├───────────────────┤ │ children * │ operation() │ │ operation() │ │ └─────────────┘ │ { for child : │◄──┘ │ children │ │ child.op(); } │ │ add(Component) │ │ remove(Component) │ └───────────────────┘The self-pointing diamond on Composite is the recursion: a composite holds a list of components, and a composite is a component, so trees nest to any depth.
When to use it#
Reach for Composite when all of these hold:
- The domain is naturally a tree — part-whole, container-contained, parent-child.
- You want client code to treat leaves and containers the same.
- Operations are recursive in shape: render, size, total cost, find-by-name, walk-and-collect.
- The tree may grow at runtime — you do not know the depth or shape up front.
Typical scenarios:
- Filesystems.
FileandDirectoryboth implementNode;Directory.size()is the sum of its children’s sizes. - GUI widget trees.
ButtonandPanelare bothComponent;Panel.render()renders each child. - Abstract syntax trees. A
Literal(3)and anAdd(left, right)both implementExpression;Add.evaluate()evaluates its operands and adds them. - Org charts and bills-of-materials. An
Employeeand aDepartment; aPartand anAssembly. Both sharecost()orheadcount(). - Graphics scenes.
Shape,Group<Shape>— translating the group translates every shape inside it.
When not to use it:
- When the structure is not actually a tree. Composite over a graph leads to cycles and double-visiting bugs.
- When leaves and containers have fundamentally different operations. Forcing one interface that fits neither cleanly is a worse design than two separate types.
- When you need type-safe operations that exist only on containers (
addChild). The pattern’s main critique is this exact trade-off — see Variants.
How it works#
The filesystem example, the textbook prompt. A Node interface; File is a leaf; Directory holds child nodes and recurses for size and listing.
import java.util.ArrayList;import java.util.List;
// --- Component: the uniform interface ---public interface Node { String name(); long sizeBytes(); void print(String indent);}
// --- Leaf: no children ---public final class FileNode implements Node { private final String name; private final long size;
public FileNode(String name, long size) { this.name = name; this.size = size; }
@Override public String name() { return name; } @Override public long sizeBytes() { return size; } @Override public void print(String indent) { System.out.println(indent + "- " + name + " (" + size + " bytes)"); }}
// --- Composite: holds children, recurses ---public final class DirectoryNode implements Node { private final String name; private final List<Node> children = new ArrayList<>();
public DirectoryNode(String name) { this.name = name; }
public DirectoryNode add(Node child) { children.add(child); return this; }
public boolean remove(Node child) { return children.remove(child); }
@Override public String name() { return name; }
@Override public long sizeBytes() { long total = 0; for (Node child : children) total += child.sizeBytes(); return total; }
@Override public void print(String indent) { System.out.println(indent + "+ " + name + "/"); for (Node child : children) child.print(indent + " "); }}
// Wiring:// DirectoryNode root = new DirectoryNode("root")// .add(new FileNode("readme.txt", 1_200))// .add(new DirectoryNode("src")// .add(new FileNode("Main.java", 4_800))// .add(new FileNode("Util.java", 2_100)));// root.print("");// System.out.println("Total: " + root.sizeBytes() + " bytes");A few details worth pointing out:
- The recursion lives in the composite.
Directory.sizeBytes()walks its children;File.sizeBytes()returns one number. Callers see a uniformnode.sizeBytes()and do not care which they have. addandremoveare on the Composite class, not the Component interface. That is the type-safe variant — see Variants below. Clients still operate onNode, but to build a tree they hold aDirectoryNodereference.- The leaf has no children list. Skip the field; do not put an empty
ArrayListon every file. A million-file tree should not allocate a million empty lists. printcarries the indent. Recursive operations often need a state argument to format their output; passing it down the tree keeps each node simple.addreturnsthis. Fluent builders make tree construction readable. This is taste, not pattern.
Variants#
The big design choice in Composite is where to put add and remove — on the Component interface (transparent) or only on the Composite class (safe). The Gang of Four discuss this trade-off as the pattern’s central tension.
| Variant | Where add/remove live | Trade-off |
|---|---|---|
| Transparent (GoF default) | On the Component interface. Leaves implement them as no-ops or throw UnsupportedOperationException. | Client code never branches on type. But the type system lets you call add on a leaf — a runtime error waiting to happen. |
| Safe (the Java-friendly form) | Only on the Composite class. Component has only the operations every node supports. | The compiler prevents leaf.add(...). Clients building trees must hold the concrete Composite reference, breaking uniformity at construction. |
| Iterator-only | No mutation in either place; trees are built via a builder and traversed via an iterator. | Immutable trees; great for concurrency. Cannot grow the tree after construction. |
Transparent
Single interface; clients never check types.
add and remove on Component.
leaf.add(child) compiles, throws at runtime.
GoF-orthodox; closer to the “uniform treatment” promise.
Safe
Operations split: shared ops on Component, structural ops on Composite.
leaf.add(child) does not compile.
Construction code must know it has a composite.
Java-idiomatic; matches how the JDK ships composites (Swing’s Container.add, not Component.add).
Swing chose safe: JComponent.add(Component) lives on the container subclass, not the top-level Component. The filesystem example above is also safe.
Example systems#
The pattern is everywhere a tree exists:
- Filesystems.
java.nio.file.Pathis the leaf-shaped facade; the underlying directory entries form a Composite tree the OS owns. - Swing and AWT.
java.awt.Componentis the leaf-or-container interface;Containeris the composite. AJFramecontaining aJPanelcontaining aJButtonis three levels deep, walked uniformly for layout, painting, and event dispatch. - The DOM.
Nodeis the interface;Elementis the composite;Textis a leaf.querySelectorAllwalks the tree treating both uniformly. - Abstract syntax trees in compilers.
ExpressionwithBinaryOp(composite) andLiteral(leaf); evaluation, type-checking, and pretty-printing are all recursive walks. - Bill-of-materials systems. A bicycle is an assembly of (wheel, frame, …); a wheel is an assembly of (rim, spokes, hub, …); a spoke is a leaf part.
totalCost()andtotalWeight()recurse. - Menu systems and configuration trees. Settings dialogs, navigation menus, JSON / YAML configuration — anything tree-shaped that needs uniform traversal.
Trade-offs#
What you gain:
- Uniform client code. No
instanceofchecks; the recursion is the composite’s responsibility, not the caller’s. - Open extension. New leaf or composite types slot in without changes to callers — symlinks, archives, mount points all become new
Nodeimplementations. - Natural fit for recursive algorithms. Size, render, find-by-name, walk-and-collect all read as one-liners that delegate to children.
- Tree depth is unbounded. The pattern does not care whether the tree is two levels or twenty.
What you pay:
- The type-safety tension. Transparent variants let leaves accept
addcalls; safe variants make tree construction non-uniform. There is no free option. - Performance on huge trees. Naive recursion blows the JVM stack at around 5,000–10,000 levels of depth; deep trees need iterative traversal with an explicit stack.
- No type-specific operations without casting. If
printneeds different colours for files vs directories, the leaf-or-composite uniformity starts leaking — either visitor-pattern it, or accept the cast. - Cycles are bugs the type system will not catch. A composite that contains its own ancestor will recurse forever. If the tree is user-built, validate during
add. - Memory overhead. Each composite carries a child collection (often
ArrayList). For millions of nodes, the list-per-node cost dominates and you want a more compact representation.
Related patterns#
- Decorator Pattern — Decorator is structurally a degenerate Composite with exactly one child. The recursion shape (wrapper is-a wrapped) is the same; the multiplicity differs.
- Adapter Pattern — Adapter wraps to change interface; Composite wraps to be the same interface. Both rely on the wrapper subtyping the wrapped, but the intent is opposite.
- Strategy Pattern — composites often hold a strategy for combining children (sum vs max vs concatenate). The two patterns compose: a configurable Composite is a Composite parameterised by a Strategy.
- OOP Fundamentals — The Four Pillars — Composite is the canonical case of polymorphism standing in for branching. The whole pattern collapses if the language cannot dispatch on the runtime type.
- Class Diagrams — drawing a Composite in UML is the textbook example of the self-association arrow; every LLD interview eventually asks you to draw one.