Composite Pattern

Treat individual objects and compositions uniformly. The recursive tree-of-things pattern that shows up everywhere.

Pattern Intermediate
9 min read
pattern structural composite tree recursion

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. File and Directory both implement Node; Directory.size() is the sum of its children’s sizes.
  • GUI widget trees. Button and Panel are both Component; Panel.render() renders each child.
  • Abstract syntax trees. A Literal(3) and an Add(left, right) both implement Expression; Add.evaluate() evaluates its operands and adds them.
  • Org charts and bills-of-materials. An Employee and a Department; a Part and an Assembly. Both share cost() or headcount().
  • 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 uniform node.sizeBytes() and do not care which they have.
  • add and remove are on the Composite class, not the Component interface. That is the type-safe variant — see Variants below. Clients still operate on Node, but to build a tree they hold a DirectoryNode reference.
  • The leaf has no children list. Skip the field; do not put an empty ArrayList on every file. A million-file tree should not allocate a million empty lists.
  • print carries the indent. Recursive operations often need a state argument to format their output; passing it down the tree keeps each node simple.
  • add returns this. 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.

VariantWhere add/remove liveTrade-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-onlyNo 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.Path is the leaf-shaped facade; the underlying directory entries form a Composite tree the OS owns.
  • Swing and AWT. java.awt.Component is the leaf-or-container interface; Container is the composite. A JFrame containing a JPanel containing a JButton is three levels deep, walked uniformly for layout, painting, and event dispatch.
  • The DOM. Node is the interface; Element is the composite; Text is a leaf. querySelectorAll walks the tree treating both uniformly.
  • Abstract syntax trees in compilers. Expression with BinaryOp (composite) and Literal (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() and totalWeight() 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 instanceof checks; 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 Node implementations.
  • 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 add calls; 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 print needs 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.
  • 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.
Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.