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).
- Interface or abstract class shared by both leaves and composites (e.g.,
- Leaf
- End node with no children, does the actual work (e.g.,
File).
- End node with no children, does the actual work (e.g.,
- Composite
- Node that holds children and delegates operations to them (e.g.,
Directory).
- Node that holds children and delegates operations to them (e.g.,
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
instanceofchecks.
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
FileSystemIteminterface. - 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
| Pros | Cons |
|---|---|
| Treats leaves and branches uniformly | Makes it difficult to restrict which types can be children |
| Simplifies client code by removing type checking | General interfaces may include methods irrelevant to leaves |
| Natural support for recursive operations | Can 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.Componentandjava.awt.Container- Document Object Model (DOM) in web browsers, where
Elementcan contain text nodes or otherElements. - UI frameworks (e.g., a
ViewcontainingTextViews andViewGroups).
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.