Composite Pattern: Treating Trees and Leaves Uniformly
How to compose objects into tree structures representing part-whole hierarchies, so clients can treat individual objects and compositions of objects through one interface.
Composite Pattern
Intent: compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects (leaves) and compositions of objects (branches/composites) uniformly, through one shared interface.
Any time your domain has a natural "this thing can contain more things of the same kind" shape — a file system, an org chart, a UI component tree, a product catalog with categories and subcategories — you have a candidate for Composite. The test is whether client code wants to say "do X" to either a single item or a whole subtree, without caring which.
A folder on your computer can contain files and other folders. When you right-click "Get Info" on a folder, it tells you the total size of everything inside, recursively — you didn't have to separately sum up every file yourself. That's Composite: the folder answers size() the same way a file does, just by delegating to its children first.
1. The problem: client code has to special-case "is this one thing or many?"
Say you're modeling a file system for a disk-usage tool. Files have a size. Folders contain files and other folders, and their size is the sum of their contents.
VIOLATION: client code branches on type everywhere
// VIOLATION: File and Folder don't share an interface, so every caller
// has to know which one it's holding and branch accordingly.
class File {
private final String name;
private final long sizeBytes;
File(String name, long sizeBytes) { this.name = name; this.sizeBytes = sizeBytes; }
long getSize() { return sizeBytes; }
}
class Folder {
private final String name;
private final List<File> files = new ArrayList<>();
private final List<Folder> subfolders = new ArrayList<>();
Folder(String name) { this.name = name; }
void addFile(File f) { files.add(f); }
void addFolder(Folder f) { subfolders.add(f); }
// Client has to recurse manually, and this method needs updating every
// time a new "container-like" type is added to the file system model.
long totalSize() {
long total = 0;
for (File f : files) total += f.getSize();
for (Folder sub : subfolders) total += sub.totalSize(); // manual recursion
return total;
}
}
// Any OTHER client that wants to compute size (a search indexer, a backup tool)
// has to reimplement this same type-branching traversal itself.
long computeSizeExternally(Object item) {
if (item instanceof File f) return f.getSize();
if (item instanceof Folder folder) {
long total = 0;
for (File f : folder.getFiles()) total += f.getSize();
for (Folder sub : folder.getSubfolders()) total += computeSizeExternally(sub);
return total;
}
throw new IllegalArgumentException("unknown type");
}Every new operation (size, search, permission check) has to be re-implemented with the same file-vs-folder branching, in every caller that needs it. And there's no way to hold "a File or a Folder, I don't care which" in a single list.
2. The fix: a shared Component interface for leaf and composite alike
// The shared interface — leaves and composites both honor this contract
interface FileSystemNode {
String getName();
long getSize();
}
// Leaf: no children, implements the operation directly
class File implements FileSystemNode {
private final String name;
private final long sizeBytes;
File(String name, long sizeBytes) { this.name = name; this.sizeBytes = sizeBytes; }
public String getName() { return name; }
public long getSize() { return sizeBytes; }
}
// Composite: holds children of the SAME interface type, implements the
// operation by delegating to each child — recursion is automatic and hidden
class Folder implements FileSystemNode {
private final String name;
private final List<FileSystemNode> children = new ArrayList<>();
Folder(String name) { this.name = name; }
void add(FileSystemNode node) { children.add(node); } // could be a File OR a Folder
void remove(FileSystemNode node) { children.remove(node); }
public String getName() { return name; }
public long getSize() {
long total = 0;
for (FileSystemNode child : children) {
total += child.getSize(); // works whether child is a File or a Folder — no branching
}
return total;
}
}// Client code treats a single file and an entire tree identically:
Folder root = new Folder("project");
Folder src = new Folder("src");
src.add(new File("Main.java", 2_048));
src.add(new File("Utils.java", 1_024));
Folder docs = new Folder("docs");
docs.add(new File("README.md", 512));
root.add(src);
root.add(docs);
root.add(new File(".gitignore", 64));
// ONE call computes the total recursively — no branching, no manual traversal
System.out.println(root.getSize()); // 2048 + 1024 + 512 + 64 = 3648
// Any function that accepts FileSystemNode works on a leaf or an entire subtree:
void printSize(FileSystemNode node) {
System.out.println(node.getName() + ": " + node.getSize());
}
printSize(root); // whole tree
printSize(src.getChildren().get(0)); // just one file — same method, same interfaceAdding a new operation like search(String query) means adding one method to the interface, implementing it once on File (check self) and once on Folder (check self + delegate to children) — every existing client that calls search() on any node, at any depth, gets correct recursive behavior automatically.
3. Handling operations that don't make sense for a Leaf
The recurring practical problem with Composite: some operations are natural for composites but meaningless (or dangerous) for leaves — add(child) on a File makes no sense. There are two standard resolutions, each with a real trade-off.
Option A — put add/remove on the shared interface, throw on File. Maximum uniformity (client code never needs to check the concrete type), but violates Liskov Substitution: a File that throws UnsupportedOperationException from add() is a subtype that doesn't honor its supertype's contract.
// Option A: uniform interface, but File.add() must throw
interface FileSystemNode {
String getName();
long getSize();
void add(FileSystemNode node); // meaningless for File
void remove(FileSystemNode node); // meaningless for File
}
class File implements FileSystemNode {
// ...
public void add(FileSystemNode node) {
throw new UnsupportedOperationException("a file cannot contain children"); // LSP violation
}
public void remove(FileSystemNode node) {
throw new UnsupportedOperationException("a file cannot contain children");
}
}Option B — keep add/remove only on Folder, not on the shared interface. Respects LSP — no forced stubs — but client code that wants to build a tree generically needs an instanceof check or a downcast when it needs to add children, which brings back some of the branching Composite was meant to eliminate.
// Option B: safe interface, generic client code needs a type check to mutate structure
interface FileSystemNode {
String getName();
long getSize();
}
class Folder implements FileSystemNode {
void add(FileSystemNode node) { children.add(node); } // only exists here
// ...
}
void addIfPossible(FileSystemNode parent, FileSystemNode child) {
if (parent instanceof Folder folder) {
folder.add(child); // type check needed — only Folder can accept children
} else {
throw new IllegalArgumentException(parent.getName() + " cannot contain children");
}
}| Option A (uniform interface) | Option B (safe interface) | |
|---|---|---|
| LSP compliant | No — File.add() must throw | Yes — File never claims a capability it doesn't have |
Client code for read-only ops (getSize, getName) | Fully uniform, no type checks anywhere | Fully uniform, no type checks anywhere |
| Client code that builds/mutates the tree | Uniform too, but risks UnsupportedOperationException at runtime | Needs instanceof/downcast to Folder before calling add/remove |
| Generally preferred for | Read-heavy trees where structure is fixed after construction (e.g. an already-parsed AST) | Trees that are actively mutated by generic code, where a runtime throw would be a real bug risk |
There's no universally "correct" answer here — it's a real trade-off between interface uniformity and type safety, and it's a great one to discuss out loud in an interview. Many production Composite implementations pick Option A for simplicity and accept the LSP violation because tree mutation only ever happens through code that already knows it's building a Folder, making the throw path effectively unreachable — document that assumption if you make it.
4. When to use vs. when it's overkill
| Use Composite when | Skip it when |
|---|---|
| Your domain genuinely has a recursive part-whole structure (files/folders, org charts, UI trees, menu structures) | The structure is only ever one level deep — a flat List<Item> is simpler and clearer |
| Client code wants to run the same operation over a single item or an entire subtree without caring which | Leaves and composites need fundamentally different operations with little shared behavior — forcing a shared interface adds no value |
| The tree depth is unknown or unbounded at compile time | The hierarchy has a small, fixed number of levels known in advance — explicit classes per level may be more readable |
5. Bridge vs. Composite
Both are structural patterns that organize objects into a hierarchy of sorts, but the shapes are different in kind. See Bridge Pattern for Bridge's full treatment.
| Bridge | Composite | |
|---|---|---|
| Structure | Two separate hierarchies connected by one composition link | One recursive hierarchy — composites contain more instances of the same component type |
| Purpose | Decouple abstraction from implementation so each can vary independently | Represent part-whole relationships and let clients treat parts and wholes uniformly |
| Recursion | None — the abstraction holds exactly one implementation, not a collection of them | Central — composites hold zero-or-more children of the same component type, often recursively |
| Motivating question | "How do I stop two hierarchies from multiplying each other?" | "How do I let a single item and a group of items be treated the same way?" |
6. Real-world composites
java.awt.Component/java.awt.Container—Container(a composite) extendsComponent(the shared type) and holds a list of childComponents, which may themselves beContainers. Calling.paint()on the root paints the entire UI tree recursively.- Any DOM tree —
Nodeis the component;Elementnodes are composites holding childNodes; text nodes are leaves.getTextContent()on any node recursively concatenates its subtree. - Compiler ASTs — an
Expressioninterface with leaf nodes (Literal,Variable) and composite nodes (BinaryOpholding a left and rightExpression);evaluate()is implemented once per node type and recurses naturally. - Organization charts / permission trees — an
Employeeinterface withIndividualContributor(leaf) andManager(composite, holding direct reports);countHeadcount()ortotalCompensation()recurse the same wayFolder.getSize()does. - Menu systems — a
MenuComponentinterface withMenuItem(leaf) andMenu(composite containing moreMenuComponents), letting a UI render nested menus with one recursive render call.
Interview Questions
- What problem does Composite solve, and what's the tell-tale domain shape that suggests you need it?
- Walk through why the naive file-system example forces every client to branch on
instanceof Filevsinstanceof Folder, and how the shared interface removes that. - Explain the trade-off between putting
add/removeon the shared component interface versus only on the composite type. Which one violates LSP, and why might a team accept that violation anyway? - How does adding a new operation (like
search()) to a Composite hierarchy compare, in effort, to adding it to the non-Composite version with manual type branching? - Give a real-world example of Composite from a Java standard library or a well-known framework, and identify the leaf type and the composite type.
- Compare Bridge and Composite. Why do people sometimes conflate them, and what's the actual structural difference?
- When would a flat
List<Item>be a better choice than Composite, even in a domain that looks hierarchical at first glance? - How would you compute an aggregate value (like total size, or total compensation) across a Composite tree without the client needing to know the tree's depth in advance?