Back to Design Patterns
Structural Pattern

Composite Pattern

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

What is the Composite Pattern?

The Composite pattern lets you compose objects into tree structures and then treat individual objects (leaves) and groups of objects (composites) through the same interface.

Think of a file system: you can call getSize() on a single file or on a whole folder — the folder recursively sums up all its children.

Problem It Solves

Type-Checking Explosion

Without Composite, every operation needs branching logic to handle leaves vs containers separately, breaking the Open/Closed Principle.

// Without Composite: manual checks everywhere
if (item instanceof File) return item.size;
else if (item instanceof Directory) return item.sumChildren();

// With Composite: recursive polymorphism
return item.getSize();

Implementation

// Composite Pattern — File System Tree

// ─── Component interface ──────────────────────────────────────
interface FileSystemItem {
    String getName();
    long   getSize();
    void   display(String indent);
}

// ─── Leaf ─────────────────────────────────────────────────────
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   display(String indent){ System.out.println(indent + "📄 " + name + " (" + size + "KB)"); }
}

// ─── Composite ────────────────────────────────────────────────
class Directory implements FileSystemItem {
    private String name;
    private java.util.List<FileSystemItem> children = new java.util.ArrayList<>();

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

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

    public String getName() { return name; }

    // Size = sum of all children — works recursively
    public long getSize() {
        return children.stream().mapToLong(FileSystemItem::getSize).sum();
    }

    public void display(String indent) {
        System.out.println(indent + "📁 " + name + " (" + getSize() + "KB total)");
        for (FileSystemItem child : children) {
            child.display(indent + "  ");  // recurse
        }
    }
}

// ─── Client — treats files and directories uniformly ──────────
public class App {
    public static void main(String[] args) {
        Directory root = new Directory("root");
        Directory src = new Directory("src");

        src.add(new File("Main.java", 12));
        src.add(new File("Config.java", 5));
        root.add(src);
        root.add(new File("pom.xml", 3));

        // Uniform operation on the whole tree
        root.display("");
        System.out.println("Total size: " + root.getSize() + "KB");
    }
}

When to Use

Tree-Structured Data

File systems, org charts, category trees, or menus.

Recursive Aggregations

Calculating totals (sums, counts, prices) over a hierarchy.

UI Hierarchies

Managing containers and widgets (DOM, Swing, Flutter).

Uniform APIs

When clients shouldn't care about the difference between a part and a whole.

Pros & Cons

Advantages

  • Simplifies client code via polymorphism
  • Easily add new component types
  • Recursive structures are handled implicitly

Disadvantages

  • Can make design over-generalized
  • Difficult to restrict specific child types
  • Deep recursion can lead to stack issues

What's Next?

Explore related patterns: