03-structural-patterns

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.

August 11, 2026
lldstructuralcompositetree-structurepart-whole

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

java
// 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

java
// 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;
    }
}
java
// 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 interface

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

java
// 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.

java
// 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 compliantNo — File.add() must throwYes — File never claims a capability it doesn't have
Client code for read-only ops (getSize, getName)Fully uniform, no type checks anywhereFully uniform, no type checks anywhere
Client code that builds/mutates the treeUniform too, but risks UnsupportedOperationException at runtimeNeeds instanceof/downcast to Folder before calling add/remove
Generally preferred forRead-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 whenSkip 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 whichLeaves 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 timeThe 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.

BridgeComposite
StructureTwo separate hierarchies connected by one composition linkOne recursive hierarchy — composites contain more instances of the same component type
PurposeDecouple abstraction from implementation so each can vary independentlyRepresent part-whole relationships and let clients treat parts and wholes uniformly
RecursionNone — the abstraction holds exactly one implementation, not a collection of themCentral — 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.ContainerContainer (a composite) extends Component (the shared type) and holds a list of child Components, which may themselves be Containers. Calling .paint() on the root paints the entire UI tree recursively.
  • Any DOM treeNode is the component; Element nodes are composites holding child Nodes; text nodes are leaves. getTextContent() on any node recursively concatenates its subtree.
  • Compiler ASTs — an Expression interface with leaf nodes (Literal, Variable) and composite nodes (BinaryOp holding a left and right Expression); evaluate() is implemented once per node type and recurses naturally.
  • Organization charts / permission trees — an Employee interface with IndividualContributor (leaf) and Manager (composite, holding direct reports); countHeadcount() or totalCompensation() recurse the same way Folder.getSize() does.
  • Menu systems — a MenuComponent interface with MenuItem (leaf) and Menu (composite containing more MenuComponents), 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 File vs instanceof Folder, and how the shared interface removes that.
  • Explain the trade-off between putting add/remove on 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?