The Composite pattern lets clients treat individual objects (leaves) and compositions of objects (containers) uniformly through a shared interface. A file system is the canonical example: a File and a Directory both have a size(), but a directory's size is the recursive sum of its children's sizes.
The hierarchy
public sealed interface Node permits FileNode, Directory {
String name();
long sum(); // size in bytes
}
public record FileNode(String name, long sum) implements Node {}
public final class Directory implements Node {
private final String name;
private final List<Node> children = new ArrayList<>();
public Directory(String name) { this.name = name; }
public String name() { return name; }
public Directory add(Node child) { children.add(child); return this; }
public long sum() {
long total = 0;
for (Node c : children) total += c.sum(); // recurses uniformly
return total;
}
}
Directory.sum() doesn't care whether each child is a FileNode or another Directory — both expose the same sum() method. That uniformity is the whole point.
Building and querying a tree
Node root = new Directory("/")
.add(new FileNode("readme.md", 1_024))
.add(new Directory("src")
.add(new FileNode("Main.java", 4_096))
.add(new FileNode("App.java", 8_192)))
.add(new Directory("logs")
.add(new FileNode("app.log", 65_536)));
System.out.println(root.sum()); // 78,848
The traversal is a textbook recursion that falls out of the polymorphic dispatch — no instanceof, no switch, no separate visitor needed for this simple aggregation.
Directory("/")
/ | \
/ | \
FileNode Directory Directory
("readme") ("src") ("logs")
/ \ |
FileNode FileNode FileNode
(Main) (App) (app.log)Why sealed
Marking Node as sealed interface ... permits FileNode, Directory gives the compiler a closed sum type. If you later add case SymbolicLink, any switch (node) that handled the other two but not the new one becomes a compile error — exactly the safety net you want for a recursive data structure.
The trade-off the interviewer will probe
Composite intentionally blurs the leaf/container distinction. A method like add(Node) makes sense on Directory but not on FileNode. Two design styles:
- Uniform interface (chosen above):
addlives only onDirectory. Callers who hold aNodereference can't add to it. Safer types, less convenient. - Transparent interface:
addlives onNode, withFileNode.addthrowingUnsupportedOperationException. Convenient but runtime errors leak in.
The Design Patterns book itself acknowledges this tension. The modern Java answer leans uniform plus sealed so the compiler enforces "add is only on Directory".