I have asked candidates to design a file system, an org chart, and a UI layout engine in 45-minute loops at the kind of companies where the LLD round is a hard gate. The ones who nail it reach for Composite without naming it, almost by instinct. Below is why, and exactly what the interviewer is checking as you draw it.
If you remember five things
The whole pattern in one glance. Everything after this is the defense of these five.
- Intent. Let a client treat one object and a tree of objects through the same interface, so the client never asks "what kind of node is this?"
- Three roles. Component (the shared interface), Leaf (no children, does the work directly), Composite (holds children and delegates to them). That is the entire cast.
- The self-reference is the signature. A Composite holds a collection of Components, and a Component can be another Composite. That loop is what makes the tree arbitrarily deep.
- Polymorphism replaces the conditional. The operation lives on the interface, the recursion hides in the structure, and the client has zero instanceof checks.
- The classic gotcha. Where do add() and remove() go? On the Component (transparent, but a Leaf.add() is a lie) or on the Composite only (safe, but the client sees two types). Know both sides cold.
1The problem it solves#
Start with the pain, because the pain is what justifies the pattern.
- You are modeling a file system. You have files, and you have folders.
- A folder holds files and other folders.
- I ask: "Give me the total size of this folder."
The naive answer leans on type checks:
long total = 0;
for (Object child : folder.getChildren()) {
if (child instanceof File) {
total += ((File) child).getSize();
} else if (child instanceof Folder) {
total += computeSize((Folder) child); // recurse
}
}
Why that instanceof ladder rots:
- 2 node types today means 2 branches. Add
SymLinktomorrow and it becomes 3. - Now multiply by every operation you support: size, delete, search, print.
- 4 operations times 3 types is 12 branches you maintain by hand.
- Every new node type edits all 4 loops. That is a direct Open/Closed violation: you cannot add a type without opening and editing existing code.
The deeper pain: the client constantly asks "what kind of thing is this?" before it can act. Composite kills that question.
Composite trades a pile of runtime type checks for a single polymorphic call. The whole value is that the client stops caring whether it holds one file or a ten-level tree. If you draw a Composite and the client still branches on type, you paid the cost and kept none of the benefit.
2The idea (mental model)#
Here is the mental model to carry into the room.
- Define one type, call it
Node. - Both the simple thing (a file) and the container (a folder) are
Nodes. - A folder's children are also just
Nodes. The folder does not care if a child is a file or another folder. It callsgetSize()and adds it up.
The recursion hides inside the structure, not in the client:
- The client calls
root.getSize()once and the tree walks itself. - A leaf returns its own size (the base case).
- A container asks each child for its size and sums the results.
- A child that happens to be a container does the same thing one level down, until it bottoms out at leaves.
The key move: the operation lives on the interface, not in the client.
- This is polymorphism replacing a conditional.
- It is the Dependency Inversion Principle in action: the client depends on the
Nodeabstraction, never on concreteFileorFolder. - The client stays dumb, which is exactly what you want.
3Structure#
Three roles, straight from the Gang of Four.
- Component. The common interface or abstract class. Declares the operations that make sense for both leaves and containers (
getSize,print,render). - Leaf. A node with no children. Implements the operation directly. A
File. - Composite. A node that holds children (each a Component) and implements the operation by delegating to those children, usually combining their results. A
Folder.
The Composite holds a collection of Components. Since Leaf and Composite are both Components, a Composite can hold either. That self-reference is what makes the tree arbitrarily deep.
DirectoryNode back to FileSystemNode. That loop is the signature of Composite: a directory aggregates nodes, and a node might be another directory. If you draw only one thing on the whiteboard, draw that loop, because it is the thing the interviewer is scanning for.4Implementation in Java#
A clean version: an interface for the common type, and child management kept on the composite only.
interface FileSystemNode {
String getName();
long getSize();
void print(String indent);
}
final class FileNode implements FileSystemNode {
private final String name;
private final long size;
FileNode(String name, long size) {
this.name = name;
this.size = size;
}
public String getName() { return name; }
public long getSize() { return size; }
public void print(String indent) {
System.out.println(indent + name + " (" + size + " bytes)");
}
}
final class DirectoryNode implements FileSystemNode {
private final String name;
private final List<FileSystemNode> children = new ArrayList<>();
DirectoryNode(String name) {
this.name = name;
}
DirectoryNode add(FileSystemNode node) {
children.add(node);
return this;
}
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(); // no instanceof, ever
}
return total;
}
public void print(String indent) {
System.out.println(indent + name + "/");
for (FileSystemNode child : children) {
child.print(indent + " ");
}
}
}
Two choices worth saying out loud in the room:
add()returnsthis, so I can chain calls. A small builder-style convenience that keeps the client readable.FileNodeisfinaland holds onlyfinalfields, so a leaf is immutable and safe to share across the tree. Immutable leaves are one less class of bug.
And the client, which is the part that matters:
DirectoryNode root = new DirectoryNode("root");
DirectoryNode src = new DirectoryNode("src");
src.add(new FileNode("Main.java", 1200))
.add(new FileNode("Utils.java", 800));
root.add(src)
.add(new FileNode("README.md", 400));
System.out.println("Total: " + root.getSize()); // Total: 2400
root.print("");
What to point at in the client:
- No type checks. The client treats
rootexactly like it would treat a single file. - When I ask for total size, the directory walks itself.
getSize()on the directory callschild.getSize()without knowing or caring whether that child is a file or another directory ten levels deep. The polymorphism does the recursion.- Cost: the traversal is O(n) in the number of nodes, and the call stack goes as deep as the tree. That stack depth matters for the cycle discussion below.
5A real-world example#
This pattern is everywhere once you see it.
- UI frameworks. A button is a leaf. A panel is a composite holding buttons and other panels.
render()ormeasure()cascades down the widget tree. React's component tree, Android'sViewGroupholdingViews, and the DOM itself are all Composite. A<div>holding text and more<div>s renders by asking its children to render. - Graphics and drawing tools. A shape is a leaf. A group of shapes you selected and dragged together is a composite. Moving the group moves every child, and nested groups just work.
- Org charts and ACLs. An employee is a leaf, a manager is a composite with reports. "Total headcount under this VP" is one call on the root.
- Build systems and expression trees. A compiler's AST is Composite. A number literal is a leaf, a
+operation is a composite holding two sub-expressions. Evaluating the root evaluates the whole tree.
Here is the runtime flow for that file-size call, so you can see the recursion unfold:
src returns 2000 because it summed its two files (1200 plus 800), and root returns 2400 after adding the 400-byte README. One call in, a fully recursive sum out.6When to use it, and when not to#
Reach for Composite when:
- You have a part-whole hierarchy and want clients to ignore the difference between a part and a whole.
- The structure is recursive: a container can hold more containers.
- You keep writing
instanceofladders to decide how to handle a node.
Skip it when:
- Your hierarchy is flat or fixed-depth. If a "group" can only ever hold leaves and never other groups, a plain
Listis simpler. Do not force a tree where there is not one. - Leaves and composites have almost nothing in common. If
getSize()is meaningless for half your node types, the shared interface becomes a lie and you end up throwingUnsupportedOperationException. That is a smell, and it means the Interface Segregation Principle is telling you to split the abstraction. - The uniformity would hide errors you care about. Sometimes you genuinely want a compile error when someone tries to add a child to a leaf. Composite's whole point is to make that distinction disappear, which is the wrong trade if the distinction is load-bearing.
My rule of thumb: if the word "nested" shows up in the requirements, Composite is probably on the table.
Reveal the answer
7Common mistakes#
Putting add() and remove() on the Component interface.
- This is the classic design tension. The Gang of Four name it transparency versus safety.
- Transparent design puts
add/removeon the Component so every node looks identical to the client, but then aFileNode.add()makes no sense: you either throw at runtime (ugly) or no-op silently (worse, bugs hide). - Safe design keeps child management on the Composite only, as above, so
FileNodesimply has noadd. - You trade some transparency for type safety, and that is the right call most of the time. Know both sides cold, because interviewers love poking at this exact tradeoff.
Forgetting the base case in recursion.
- A leaf must return a concrete value without recursing.
- If
FileNode.getSize()accidentally loops over children it does not have, or you blur leaf and composite, you get infinite recursion or wrong sums. - Keep the leaf dead simple: it is the base case.
Caching size without invalidation.
- Tempting to cache a directory's total size, turning an O(n) walk into an O(1) read.
- Fine, but now every
add()andremove()must invalidate the cache up the whole parent chain, which means your nodes need a parent pointer. - Most candidates add the cache and forget the invalidation.
- If you mention caching in an interview, mention invalidation in the same breath, or you just introduced a stale-data bug on purpose.
Letting the client still type-check.
- I have seen people build a perfect Composite and then write
if (node instanceof DirectoryNode)in the client anyway. - If you are still asking what type a node is, you threw away the whole benefit.
- The client should only ever call interface methods.
Cycles in the tree.
- If you let a directory add itself, or add an ancestor, you made a graph, not a tree, and
getSize()recurses until the stack overflows. - For user-facing structures, guard in
add(): walk up the parent chain and reject the insert if the new child is already an ancestor.
Building a flawless Composite and then writing if (node instanceof DirectoryNode) in the client. You spent the whole design removing type checks, then reintroduced the exact thing the pattern exists to kill. The interviewer reads it as "memorized the diagram, did not understand the point."
When you reach for caching, I am listening for the word "invalidate" in the next breath. Candidates who add an O(1) size cache and move on have quietly planted a stale-data bug. Candidates who say "and every add/remove invalidates up the parent chain, so nodes need a parent pointer" have actually run this code.
8Where it shows up in interviews#
Composite is a go-to answer for a cluster of LLD prompts.
- Design a file system. The canonical one. Files and directories,
getSize,search,delete. Composite is expected, and not drawing it reads as a miss. - Design a UI layout or rendering engine. Widgets containing widgets.
render(),measure(). - Design an org chart or company hierarchy. Employees and managers, "count all reports."
- Design a menu system (restaurant menu, nav menu). Menu items and sub-menus.
- Expression evaluator or calculator. Numbers and operators as a tree.
When you spot one, say the quiet part out loud:
- "I will model this as a Composite. There is a common
Nodetype, leaves implement the operation directly, and containers delegate to their children." - That single sentence tells the interviewer you know the pattern cold.
Reciting the GoF roles word for word impresses no one. Writing the getSize() that walks the tree with zero instanceof checks impresses everyone.
Then expect the follow-up. It is almost always "Where do add and remove go?" (the transparency-versus-safety trade) or "How would you add a search operation?" (a soft nudge toward Visitor, because adding operations to a Composite tree without touching every node class is exactly what Visitor is for). Connect those two patterns in the room and you look senior.
The last thing: do not over-engineer it. A clean interface, one leaf, one composite with a List of children, and a recursive operation. That is a complete, correct answer.
"Where do add() and remove() go, on the Component or the Composite?"
"How would you add a search operation without editing every node class?"
"Someone adds a directory to itself. What happens, and how do you prevent it?"
"Can you make getSize() O(1) instead of O(n)?"
"Half my node types do not support getSize(). Is Composite still right?"
What you can now explain in the room
- Why an instanceof ladder over node types violates Open/Closed, and how Composite removes it.
- The three roles (Component, Leaf, Composite) and why the Composite-holds-Component self-reference is the signature of the pattern.
- How the recursion lives in the structure, so the client makes one call and does zero type checks.
- The transparency-versus-safety trade for add() and remove(), and why safe is the usual default.
- The common failure modes: missing base case, caching without invalidation, cycles turning the tree into a graph, and a client that still type-checks.
- When to skip Composite (flat hierarchies, leaves and composites with nothing in common) and when to reach for Visitor instead.
Next, look at the Visitor pattern (adding operations to a Composite tree), the Decorator pattern (which shares the recursive-wrapping shape), and the "Design a File System" LLD problem to put this to work.
Get a prep plan built around your Composite Pattern interview
Tell us the role and your interview date. We will send back a dated, day by day plan. Free.