The Composite Pattern is a structural design pattern that lets you compose objects into tree structures and then work with those structures as if they were individual objects. Think of a file system — a directory can contain files and other directories. Whether you ask for the size of a single file or an entire directory tree, you use the same operation. The composite handles the recursion for you.
This pattern is essential whenever you deal with hierarchical or tree-like structures where individual items and groups of items should be treated uniformly.
You are building a file system utility that calculates the total size of files and directories. A file has a fixed size, but a directory contains other files and subdirectories, each of which may contain more files and subdirectories. You need to calculate the total size at any level of the tree.
Without a unified interface, client code must constantly check whether it is dealing with a file or a directory, leading to scattered conditional logic and code that breaks every time a new type of node is added.
Define a common interface (FileSystemComponent) with a getSize() method. Files implement it by returning their own size. Directories implement it by summing the sizes of all their children — which can be files or other directories. Client code simply calls getSize() on any component without caring about its type.
The recursive structure is handled naturally by the composite, and the client code remains clean and simple.
The Composite Pattern follows the Uniform Treatment Principle — clients should not need to distinguish between individual objects and compositions of objects. It also leverages recursive composition, letting you build complex structures from simple, uniform building blocks.
Here we model a file system where files and directories share a common interface.
import java.util.ArrayList;
import java.util.List;
// Component interface
public interface FileSystemComponent {
String getName();
long getSize();
void display(String indent);
}
// Leaf: individual file
public class FileItem implements FileSystemComponent {
private final String name;
private final long size;
public FileItem(String name, long size) {
this.name = name;
this.size = size;
}
@Override
public String getName() { return name; }
@Override
public long getSize() { return size; }
@Override
public void display(String indent) {
System.out.printf("%s📄 %s (%d bytes)%n", indent, name, size);
}
}
// Composite: directory containing files and subdirectories
public class Directory implements FileSystemComponent {
private final String name;
private final List<FileSystemComponent> children = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
public Directory add(FileSystemComponent component) {
children.add(component);
return this; // Fluent API
}
public void remove(FileSystemComponent component) {
children.remove(component);
}
@Override
public String getName() { return name; }
@Override
public long getSize() {
// Recursively sums sizes of all children
return children.stream()
.mapToLong(FileSystemComponent::getSize)
.sum();
}
@Override
public void display(String indent) {
System.out.printf("%s📁 %s/ (%d bytes total)%n", indent, name, getSize());
for (FileSystemComponent child : children) {
child.display(indent + " ");
}
}
}
// Usage
public class Main {
public static void main(String[] args) {
// Build a file system tree
Directory root = new Directory("project");
Directory src = new Directory("src");
src.add(new FileItem("Main.java", 2400));
src.add(new FileItem("Utils.java", 1200));
Directory config = new Directory("config");
config.add(new FileItem("application.yml", 350));
config.add(new FileItem("logback.xml", 500));
src.add(config);
Directory test = new Directory("test");
test.add(new FileItem("MainTest.java", 1800));
root.add(src);
root.add(test);
root.add(new FileItem("README.md", 600));
root.add(new FileItem("pom.xml", 3200));
// Uniform interface - works the same for files and directories
root.display("");
System.out.println("\nTotal project size: " + root.getSize() + " bytes");
// Works on any subtree too
System.out.println("Source directory size: " + src.getSize() + " bytes");
}
}
The same file system composite scenario in Python.
from abc import ABC, abstractmethod
# Component interface
class FileSystemComponent(ABC):
@abstractmethod
def get_name(self) -> str:
pass
@abstractmethod
def get_size(self) -> int:
pass
@abstractmethod
def display(self, indent: str = "") -> None:
pass
# Leaf: individual file
class FileItem(FileSystemComponent):
def __init__(self, name: str, size: int):
self._name = name
self._size = size
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
return self._size
def display(self, indent: str = "") -> None:
print(f"{indent}file: {self._name} ({self._size} bytes)")
# Composite: directory containing files and subdirectories
class Directory(FileSystemComponent):
def __init__(self, name: str):
self._name = name
self._children: list[FileSystemComponent] = []
def add(self, component: FileSystemComponent) -> "Directory":
self._children.append(component)
return self # Fluent API
def remove(self, component: FileSystemComponent) -> None:
self._children.remove(component)
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
# Recursively sums sizes of all children
return sum(child.get_size() for child in self._children)
def display(self, indent: str = "") -> None:
print(f"{indent}dir: {self._name}/ ({self.get_size()} bytes total)")
for child in self._children:
child.display(indent + " ")
# Usage
if __name__ == "__main__":
# Build a file system tree
root = Directory("project")
src = Directory("src")
src.add(FileItem("Main.java", 2400))
src.add(FileItem("Utils.java", 1200))
config = Directory("config")
config.add(FileItem("application.yml", 350))
config.add(FileItem("logback.xml", 500))
src.add(config)
test = Directory("test")
test.add(FileItem("MainTest.java", 1800))
root.add(src)
root.add(test)
root.add(FileItem("README.md", 600))
root.add(FileItem("pom.xml", 3200))
# Uniform interface - works the same for files and directories
root.display()
print(f"\nTotal project size: {root.get_size()} bytes")
# Works on any subtree too
print(f"Source directory size: {src.get_size()} bytes")
Component and Container form a composite tree where containers hold other componentsjava.io.File — Represents both files and directories with shared operations like length() and listFiles()ast module — Abstract Syntax Trees where nodes contain other nodes uniformlyGrantedAuthority hierarchies where roles can contain other roles