Structural

Composite

Compose objects into tree structures to represent part-whole hierarchies — treat individual objects and compositions uniformly.

Intermediate Pattern #08 of 23

What is the Composite Pattern?

Composite lets clients treat individual objects (leaves) and compositions of objects (composites) through the same interface. The client never needs to distinguish between a single item and a group of items — both respond to the same operations.

It builds tree structures where every node (composite) can contain children that are either leaves or other composites, allowing recursive composition of arbitrary depth.

Real-world analogy: A company org chart. An individual employee (leaf) and a department (composite containing employees or sub-departments) both respond to getSalary(). Calling it on a department sums up the salaries of everyone inside it recursively.

The Problem It Solves

Consider a file system. You have File objects and Folder objects. Folders can contain Files or other Folders. To calculate total size, you'd have to write one code path for files and a different recursive one for folders — and repeat this every time you need a new operation.

Composite makes both File and Folder implement the same FileSystem interface. getSize() on a File returns its own size; on a Folder it sums children recursively. Client calls the same method on both.

Participants

Component (interface)
Declares the common interface for both leaves and composites. May include default implementations for child management methods.
Leaf
A basic element with no children. Does the actual work. Has no child management — it's the end node of the tree.
Composite
A container that stores child Components (leaves or other composites). Implements child management (add/remove). Delegates operations to children and aggregates results.
Client
Works with all elements through the Component interface — never checks if it's dealing with a leaf or composite.

Visual Flow Diagram

«interface» Component operation() / getSize() Leaf (File, Employee) operation() — does work Composite (Folder, Department) children: List<Component> operation() — loops children contains 0..* Component Tree: Folder → [File, File, Folder → [File, Folder → [File]]] — any depth

Java Code Example

Java — File System Example
// Component — uniform interface
public interface FileSystemItem {
    String getName();
    long   getSize();
    void   print(String indent);
}

// Leaf — no children
public class File implements FileSystemItem {
    private String name;
    private long   size;

    public File(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 + "KB)");
    }
}

// Composite — contains children
public class Folder implements FileSystemItem {
    private String name;
    private List<FileSystemItem> children = new ArrayList<>();

    public Folder(String name) { this.name = name; }

    public void add(FileSystemItem item)    { children.add(item); }
    public void remove(FileSystemItem item) { children.remove(item); }

    public String getName() { return name; }

    // Composite operation: aggregates from all children recursively
    public long getSize() {
        return children.stream()
            .mapToLong(FileSystemItem::getSize)
            .sum();
    }

    public void print(String indent) {
        System.out.println(indent + "📁 " + name);
        children.forEach(c -> c.print(indent + "  ")); // recursive!
    }
}

// Client — never distinguishes File from Folder
public class Main {
    public static void main(String[] args) {
        Folder root = new Folder("root");

        Folder src = new Folder("src");
        src.add(new File("Main.java",  12));
        src.add(new File("Utils.java", 8));

        Folder test = new Folder("test");
        test.add(new File("MainTest.java", 5));

        root.add(src);
        root.add(test);
        root.add(new File("README.md", 2));

        root.print("");
        // 📁 root
        //   📁 src
        //     📄 Main.java (12KB)
        //     📄 Utils.java (8KB)
        //   📁 test
        //     📄 MainTest.java (5KB)
        //   📄 README.md (2KB)

        System.out.println("Total: " + root.getSize() + "KB"); // 27KB
    }
}

When to Use / Avoid

✓ Use When

  • You need to represent part-whole hierarchies (trees)
  • Clients should treat leaf and composite objects uniformly
  • You want recursive operations to propagate through a tree automatically
  • Building UI component trees, org charts, file systems, menus

✕ Avoid When

  • Components are too different to share a common interface meaningfully
  • You need to restrict what types can be added as children at compile time
  • The hierarchy is flat — no benefit over a simple list

Real-World Examples

Pros & Cons

Pros

  • Uniform treatment — client code is simpler, works on leaves and composites identically
  • Open/Closed — add new leaf or composite types without changing client
  • Recursive operations propagate naturally through the tree

Cons

  • Overly general interface — hard to restrict what types of children a composite accepts
  • Leaves must implement child-management methods even if meaningless for them
  • Deep trees can cause performance issues or stack overflows in recursive operations

How Composite Can Be Broken

⚠ Attack Vectors

  • Circular references: A Composite is added as a child of its own descendant — recursive operations like getSize() loop infinitely causing a StackOverflowError
  • Adding a composite to itself: folder.add(folder) — simplest form of circular reference, crashes on any recursive call
  • Type-checking children: Client uses instanceof to distinguish Leaf from Composite inside operations — defeats the whole point of the uniform interface
  • Leaf implementing add/remove: If Leaf implements child management and throws at runtime, client only discovers the error late — not at compile time
  • Deep unbounded trees: Very deep recursive trees cause stack overflow on operations that traverse top-to-bottom without tail recursion or iteration

✓ Prevention

  • Cycle detection in add(): Walk the ancestor chain before adding a child — reject any child that is an ancestor of the current composite
  • Self-add guard: In add(), explicitly check if (item == this) throw new IllegalArgumentException("Cannot add to itself")
  • Never use instanceof in client: All operations should be on the Component interface — if you're checking types, the interface needs a method added
  • Separate interface for composites: Use a Composite sub-interface that adds add()/remove() — Leaf never implements these, catching misuse at compile time
  • Iterative traversal for deep trees: Replace recursive calls with an explicit stack (Deque) to avoid StackOverflowError on very deep hierarchies
Java — Break & Fix
// ❌ BREAKING — circular reference → StackOverflow
Folder a = new Folder("a");
Folder b = new Folder("b");
a.add(b);
b.add(a); // ❌ cycle! getSize() loops forever

// ✅ FIX — ancestor check in add()
public void add(FileSystemItem item) {
    if (item == this)
        throw new IllegalArgumentException("Cannot add folder to itself");
    if (isAncestor(item))
        throw new IllegalArgumentException("Circular reference detected");
    children.add(item);
}

private boolean isAncestor(FileSystemItem candidate) {
    if (!(candidate instanceof Folder)) return false;
    Folder f = (Folder) candidate;
    for (FileSystemItem child : f.children) {
        if (child == this || isAncestor(child)) return true;
    }
    return false;
}

// ❌ BREAKING — client uses instanceof, defeats pattern
void process(FileSystemItem item) {
    if (item instanceof Folder) { /* folder logic */ }
    else                         { /* file logic */ }
}

// ✅ FIX — add method to the interface, let each type handle itself
public interface FileSystemItem {
    void process(); // File and Folder implement differently
}

// ✅ FIX — iterative traversal for deep trees
public long getSizeIterative(Folder root) {
    long total = 0;
    Deque<FileSystemItem> stack = new ArrayDeque<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        FileSystemItem item = stack.pop();
        total += item.getSize();
        if (item instanceof Folder)
            ((Folder) item).children.forEach(stack::push);
    }
    return total;
}

How Other Patterns Relate

Interview Cheat Sheet

  1. What: Tree structure where leaves and composites implement the same interface. Client calls the same operation on either — composites delegate recursively to children, leaves do the actual work.
  2. How: Component interface with operation(). Leaf implements it directly. Composite stores List<Component> and loops children in its operation(). Both look identical to the client.
  3. Key interview point: The critical benefit is uniform treatment — no if/else for leaf vs composite. Critical risk is circular references — always guard add() with ancestor checks.