SSourav Saha
HomeExperienceSoftware DesignSystem DesignLearningBooksToolsContact
SSourav Saha

Building scalable backend systems, distributed infrastructure, and cloud-native applications.

Navigation

  • Home
  • Experience
  • Software Design
  • System Design
  • Learning

More

  • Books
  • Tools
  • Contact

Connect

  • LinkedIn
  • Email

© 2026 Sourav Saha. All rights reserved.

Built with using Next.js

Software DesignComposite Pattern
Software DesignStructural Patterns

Composite Pattern

Compose objects into tree structures to treat individual objects and compositions uniformly.

November 1, 20245 min read
design-patternsjavacompositestructural

One-line Definition

Encapsulates tree structures so clients can treat individual leaves and entire branches uniformly through a shared interface.


Problem

Imagine building an ordering system where a Box can contain Products or smaller Boxes. If the client wants to calculate the total price, it must manually unpack the boxes, check if an item is a Product (add its price) or a Box (open it and loop through its contents). This forces the client to handle the complex recursion and type-checking. The Composite pattern solves this by letting both Boxes and Products share a common getPrice() method.


Real-world Analogy

Think of a corporate org chart. A CEO manages VPs, who manage directors, who manage individual contributors. When the CEO asks "How many people are in Engineering?", the VP doesn't count one by one — they ask each director, who asks each team. The composite structure lets you ask the same question at any level and get the right answer recursively.


Structure

  • Client
    • Works with all elements through the Component interface, regardless of whether they are leaves or branches.
  • Component
    • Interface or abstract class shared by both leaves and composites (e.g., FileSystemItem).
  • Leaf
    • End node with no children, does the actual work (e.g., File).
  • Composite
    • Node that holds children and delegates operations to them (e.g., Directory).

Diagram

classDiagram
    class Client {
    }
    class Component {
        <<interface>>
        +execute()
    }
    class Leaf {
        +execute()
    }
    class Composite {
        -List~Component~ children
        +add(c: Component)
        +execute()
    }
    
    Client --> Component
    Leaf ..|> Component
    Composite ..|> Component
    Composite o-- Component : contains

Code Walkthrough

Notice that Main (the client) calls getSize() on the root Directory. The directory automatically delegates the call down the tree to all its files and subdirectories.

interface FileSystemItem {
    String getName();
    int getSize();
    void display(String indent);
}

class File implements FileSystemItem {
    private final String name;
    private final int size;

    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }

    @Override
    public String getName() { return name; }
    @Override
    public int getSize() { return size; }
    @Override
    public void display(String indent) {
        System.out.println(indent + "📄 " + name + " (" + size + " KB)");
    }
}

class Directory implements FileSystemItem {
    private final String name;
    private final List<FileSystemItem> children = new ArrayList<>();

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

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

    @Override
    public String getName() { return name; }

    @Override
    public int getSize() {
        return children.stream().mapToInt(FileSystemItem::getSize).sum();
    }

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

class Main {
    public static void main(String[] args) {
        Directory root = new Directory("root");

        Directory src = new Directory("src");
        src.add(new File("Main.java", 10));
        src.add(new File("Utils.java", 5));

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

        root.display("");
        System.out.println("Total size: " + root.getSize() + " KB");
    }
}

Bad vs Good

Bad Approach

Problems

  • Client must check whether each item is a file or directory.
  • Adding a new type (e.g., Symlink) requires modifying the recursive logic.
  • Deep nesting requires complex instanceof checks.
class FileExplorer {
    public int calculateSize(Object item) {
        if (item instanceof File) {
            return ((File) item).getSize();
        } else if (item instanceof Directory) {
            int total = 0;
            for (Object child : ((Directory) item).getChildren()) {
                total += calculateSize(child); // recursive if-else
            }
            return total;
        }
        return 0;
    }
}

Better Approach

Improvements

  • Client relies strictly on the FileSystemItem interface.
  • Adding new node types just requires implementing the interface, leaving the client untouched.
  • Recursion is handled naturally by the objects themselves.
interface FileSystemItem {
    int getSize();
}

// Usage — no instanceof, no type checking
FileSystemItem item = getAnyItem();
System.out.println("Size: " + item.getSize());

Pros vs Cons

ProsCons
Treats leaves and branches uniformlyMakes it difficult to restrict which types can be children
Simplifies client code by removing type checkingGeneral interfaces may include methods irrelevant to leaves
Natural support for recursive operationsCan overcomplicate simple, flat collections
Follows Open/Closed Principle for new components
Perfectly models real-world tree structures

When to Use

  • Your data naturally forms a tree or hierarchical structure (e.g., file systems, UI trees, org charts).
  • You want clients to ignore the differences between individual objects and collections of objects.
  • You need to perform cascading operations down a tree.

When Not to Use

  • The hierarchy is extremely shallow or non-existent (just use a List).
  • You strictly need to enforce that a composite can only contain specific types of leaves (the shared interface makes this hard to type-check at compile time).

Real-world Examples

  • java.awt.Component and java.awt.Container
  • Document Object Model (DOM) in web browsers, where Element can contain text nodes or other Elements.
  • UI frameworks (e.g., a View containing TextViews and ViewGroups).

Key Takeaway

The Composite Pattern makes tree-structured data trivial to navigate by giving both the leaves and the branches the exact same interface. Use it when you want to execute recursive operations without writing messy instanceof checks. Avoid it if your data is flat and doesn't require a hierarchical structure.

PreviousTemplate Method PatternNextCommand Pattern