Home › Low-Level Design › Composite Pattern
C

Composite Pattern

Treat a single thing and a group of things the same way. That one sentence is the whole pattern. Everything else is plumbing to make that promise hold at runtime.

Low-Level DesignMedium9 min read

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.

  1. 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?"
  2. 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.
  3. 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.
  4. Polymorphism replaces the conditional. The operation lives on the interface, the recursion hides in the structure, and the client has zero instanceof checks.
  5. 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 SymLink tomorrow 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.

The one thing this pattern buys you

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 calls getSize() 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 Node abstraction, never on concrete File or Folder.
  • 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.

classDiagram class FileSystemNode { <> +getName() String +getSize() long +print(indent) void } class FileNode { -String name -long size +getSize() long } class DirectoryNode { -String name -List children +add(node) void +remove(node) void +getSize() long } FileSystemNode <|.. FileNode FileSystemNode <|.. DirectoryNode DirectoryNode o-- FileSystemNode
Notice the aggregation arrow looping from 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() returns this, so I can chain calls. A small builder-style convenience that keeps the client readable.
  • FileNode is final and holds only final fields, 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 root exactly like it would treat a single file.
  • When I ask for total size, the directory walks itself.
  • getSize() on the directory calls child.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() or measure() cascades down the widget tree. React's component tree, Android's ViewGroup holding Views, 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:

sequenceDiagram participant Client participant Root as root Directory participant Src as src Directory participant File as Main.java Client->>Root: getSize() Root->>Src: getSize() Src->>File: getSize() File-->>Src: 1200 Src-->>Root: 2000 Root-->>Client: 2400
The client made one call. The structure did the rest. Watch the numbers climb back up: 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 instanceof ladders 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 List is 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 throwing UnsupportedOperationException. 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.

Pause before the mistakes section: you want to put add() and remove() on the shared Component interface so every node looks identical to the client. What exactly does that cost you?
Reveal the answer
A `FileNode.add(child)` now compiles but makes no sense. You either throw at runtime (ugly, and the client cannot tell it is coming) or no-op silently (worse, because bugs hide). You bought transparency (one type for the client) and paid with type safety (the compiler can no longer stop a nonsense call). The Gang of Four call this transparency versus safety, and picking the safe side (child management on the Composite only) is the right default most of the time.

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/remove on the Component so every node looks identical to the client, but then a FileNode.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 FileNode simply has no add.
  • 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() and remove() 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.
Red flag that fails the round

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."

From the interviewer's chair

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 Node type, 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.

Follow-ups the interviewer will ask (and your answers)
"Where do add() and remove() go, on the Component or the Composite?"
That is the transparency-versus-safety trade. Transparent puts them on the Component so every node looks identical to the client, but then Leaf.add() is a lie you either throw on or silently ignore. Safe keeps child management on the Composite only, so a Leaf has no add() at all. I default to safe: I would rather the compiler stop a nonsense call than discover it at runtime. I only go transparent if the client genuinely must iterate over mixed nodes and manipulate children uniformly.
"How would you add a search operation without editing every node class?"
That is the nudge toward Visitor. Adding behavior to a Composite tree by touching every node class is painful when you expect many new operations. A Visitor pulls the operation out into its own class with a visit method per node type, so a new operation is a new Visitor, not an edit to File and Directory. The tradeoff flips: Composite makes adding node types cheap and operations expensive, Visitor makes adding operations cheap and node types expensive. I name which axis is going to change more and pick accordingly.
"Someone adds a directory to itself. What happens, and how do you prevent it?"
getSize() recurses forever and the stack overflows, because you turned a tree into a graph with a cycle. I guard in add(): walk up the parent chain of the target and reject the insert if the node being added is already an ancestor (or is the node itself). That requires a parent pointer, which I would add anyway if I need upward navigation or cache invalidation.
"Can you make getSize() O(1) instead of O(n)?"
Yes, by caching the subtree total on each directory, but the cost moves to writes. Every add() and remove() must invalidate the cached size up the whole parent chain, which means each node needs a parent pointer. If reads dominate and the tree is fairly static, the cache is a clear win. If the tree mutates constantly, the invalidation bookkeeping can cost more than it saves. The bug to avoid is caching without invalidating, which silently returns stale sizes.
"Half my node types do not support getSize(). Is Composite still right?"
That is a warning sign, not a yes. If the shared interface only makes sense for some nodes, you are forcing an abstraction and will end up throwing UnsupportedOperationException, which Interface Segregation says to avoid. I would split the interface so each operation lives where it is meaningful, or drop Composite entirely if the nodes really have nothing in common. Composite earns its keep only when the operation genuinely applies to both leaves and containers.

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.

Thanks. Check your inbox, your Composite Pattern prep plan is on the way.