Composite

June 17, 2026 in patterns 5 minutes

A intermediate-level guide to Composite: before-and-after java code and diagrams for a CS student.

Imagine you are building a file explorer. You have individual files, which represent the “leaves” or end-points of your data, and folders, which act as containers that hold both files and other folders. When you want to calculate the total size of a directory, you cannot simply look at one number; you must drill down into every sub-folder and sum up every individual file.

The difficulty arises when your code treats “a single file” and “a folder containing files” as two completely different species that require different handling logic.

import java.util.*;

class File {
    private String name;
    private long size;

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

    public long getSize() { return size; }
    public String getName() { return name; }
}

class Folder {
    private String name;
    private List<Object> contents = new ArrayList<>();

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

    public void add(Object item) { contents.add(item); }
    public String getName() { return name; }
    public List<Object> getContents() { return contents; }
}

class FileSystemService {
    public long calculateTotalSize(Object item) {
        if (item instanceof File) {
            return ((File) item).getSize();
        } else if (item instanceof Folder) {
            long total = 0;
            for (Object content : ((Folder) item).getContents()) {
                total += calculateTotalSize(content);
            }
            return total;
        }
        throw new IllegalArgumentException("Unknown type");
    }
}

The current implementation suffers from a “Switch Statements” smell (specifically, using if/else to check types). Every time the FileSystemService wants to perform an operation like calculateTotalSize, it must ask: “Are you a File? If so, do this. Are you a Folder? If so, loop through your contents and ask them for their size.”

As the hierarchy grows deeper or more complex, these manual type-checks become a maintenance nightmare. To fix this, we use the refactoring called Replace Conditional with Polymorphism. Instead of the service deciding how to handle each type, we let the objects decide for themselves through a shared interface.

  classDiagram
    FileSystemService ..> Object : uses
    Object <|-- File
    Object <|-- Folder
    Folder *-- Object : contains

The Composite Pattern: Uniformity Through Recursion

The Composite pattern allows us to treat individual objects and compositions of objects uniformly. This is achieved by creating a shared interface that both the “Leaf” (the single item) and the “Composite” (the container) implement.

The Anatomy of a Composite

To build this, we need three specific roles:

  1. Component: An interface or abstract class that defines the common behavior (like getSize() and getName()) for both items and containers.
  2. Leaf: A concrete class that represents an individual item with no children (e.g., a File).
  3. Composite: A concrete class that contains a collection of Components (e.g., a Folder).

When you call getSize() on a Composite, it doesn’t need to know if its children are files or other folders; it simply tells every child: “Give me your size.” This is called recursive composition, where a structure is built by nesting objects within themselves.

import java.util.*;

interface FileSystemComponent {
    long getSize();
    String getName();
}

class File implements FileSystemComponent {
    private String name;
    private long size;

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

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

class Folder implements FileSystemComponent {
    private String name;
    private List<FileSystemComponent> children = new ArrayList<>();

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

    public void add(FileSystemComponent component) { children.add(component); }
    @Override
    public long getSize() { 
        long total = 0;
        for (FileSystemComponent child : children) {
            total += child.getSize();
        }
        return total;
    }
    @Override
    public String getName() { return name; }
}

class FileSystemService {
    public long calculateTotalSize(FileSystemComponent component) {
        return component.getSize();
    }
}

In this refactored version, the FileSystemService is incredibly simple. It no longer cares about types; it only cares that the object passed to it satisfies the FileSystemComponent interface. The complexity of traversing the tree is moved out of the service and into the objects themselves.

When we call getSize() on a Folder, it triggers a chain reaction. The folder asks its children for their sizes, and if those children are also folders, they ask their children, until the calls reach the File objects at the bottom of the tree.

  sequenceDiagram
    participant F as Folder
    participant C1 as FileComponent (Leaf)
    participant C2 as Folder (Composite)
    participant L as File (Leaf)

    F ->> C1: getSize()
    C1 -->> F: size

    F ->> C2: getSize()
    C2 ->> L: getSize()
    L -->> C2: size
    C2 -->> F: totalSize

When to Use It, and When Not To

The Composite pattern is highly effective when your data naturally forms a tree structure—such as organizational charts, UI widget hierarchies, or XML documents. It provides high uniformity, allowing you to add new types of components without ever changing the code that uses them (adhering to the Open-Closed Principle).

However, there is a tension between uniformity and interface integrity. By forcing both Files and Folders to implement the same interface, you face a choice:

  • You can put structural methods like add() or remove() in the base FileSystemComponent interface. This makes everything uniform, but it creates a “Uniformity Trap” where a single File object is forced to implement an add() method that makes no sense and throws an error.
  • Alternatively, you can keep structural methods out of the base interface. This keeps your Leaf classes clean, but it means you might have to perform type-casting again if you want to add a file specifically to a folder.

Choose the approach that best fits how much safety you need versus how much simplicity you desire in your hierarchy.

Takeaways

  • Use the Composite pattern when you need to represent “part-whole” hierarchies where clients should treat individual items and collections identically.
  • Replace type-checking if/else chains with polymorphic method calls defined in a shared interface.
  • Be wary of adding management methods (like add() or remove()) to your base interface, as this can force leaf objects to implement meaningless behavior.

Usage

Demonstrating tree traversal via the Composite pattern

Folder root = new Folder("root");
root.add(new File("config.txt", 50));
Folder sub = new Folder("docs");
sub.add(new File("resume.pdf", 150));
root.add(sub);

long totalSize = service.calculateTotalSize(root); // 200