You’re building a file system browser. Folders contain files and other folders. When you calculate folder size, you sum up file sizes and recursively add subfolder sizes. Delete a folder and you delete everything inside.
The client shouldn’t care whether it’s dealing with a file or a folder. Both respond to the same operations.
What is the Composite Pattern?
Composite lets you treat individual objects and compositions of objects uniformly. Both leaf objects and composite containers implement the same interface. The client doesn’t know if it’s working with a single item or a tree.

The Composite holds children that are also Components. This creates recursive tree structures where operations propagate through the hierarchy.
When to Use Composite
| Use Composite When | Skip Composite When |
|---|---|
| You have tree-like structures | Structure is flat |
| Clients should treat parts and wholes the same | Leaves and containers are fundamentally different |
| You need recursive operations | Recursion isn’t needed |
| Hierarchies vary in depth | Fixed, shallow structure |
Composite is about structure, not behavior. It creates part-whole hierarchies where leaves and branches share a common interface.
Implementation
File System Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
public interface FileSystemNode {
String getName();
long getSize();
void delete();
void display(String indent);
}
public class File implements FileSystemNode {
private final String name;
private final long size;
public File(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 delete() {
System.out.println("Deleting file: " + name);
}
@Override
public void display(String indent) {
System.out.println(indent + "📄 " + name + " (" + size + " bytes)");
}
}
public class Folder implements FileSystemNode {
private final String name;
private final List<FileSystemNode> children = new ArrayList<>();
public Folder(String name) {
this.name = name;
}
public void add(FileSystemNode node) {
children.add(node);
}
public void remove(FileSystemNode node) {
children.remove(node);
}
public List<FileSystemNode> getChildren() {
return Collections.unmodifiableList(children);
}
@Override
public String getName() {
return name;
}
@Override
public long getSize() {
// Recursive - sum all children
return children.stream()
.mapToLong(FileSystemNode::getSize)
.sum();
}
@Override
public void delete() {
// Recursive - delete all children first
for (FileSystemNode child : new ArrayList<>(children)) {
child.delete();
}
System.out.println("Deleting folder: " + name);
children.clear();
}
@Override
public void display(String indent) {
System.out.println(indent + "📁 " + name + " (" + getSize() + " bytes)");
for (FileSystemNode child : children) {
child.display(indent + " ");
}
}
}
Usage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Build file system structure
Folder root = new Folder("root");
Folder documents = new Folder("documents");
documents.add(new File("resume.pdf", 50000));
documents.add(new File("cover_letter.docx", 25000));
Folder photos = new Folder("photos");
photos.add(new File("vacation.jpg", 2500000));
photos.add(new File("family.png", 3200000));
Folder work = new Folder("work");
work.add(new File("project.xlsx", 150000));
photos.add(work); // Nested folder
root.add(documents);
root.add(photos);
root.add(new File("readme.txt", 1000));
// Display entire tree
root.display("");
// Output:
// 📁 root (5926000 bytes)
// 📁 documents (75000 bytes)
// 📄 resume.pdf (50000 bytes)
// 📄 cover_letter.docx (25000 bytes)
// 📁 photos (5850000 bytes)
// 📄 vacation.jpg (2500000 bytes)
// 📄 family.png (3200000 bytes)
// 📁 work (150000 bytes)
// 📄 project.xlsx (150000 bytes)
// 📄 readme.txt (1000 bytes)
// Uniform treatment
System.out.println("Total size: " + root.getSize()); // Works on any node
System.out.println("Documents size: " + documents.getSize());
System.out.println("Single file size: " + documents.getChildren().get(0).getSize());
UI Component Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
public interface UIComponent {
void render();
int getWidth();
int getHeight();
}
public class Button implements UIComponent {
private final String label;
private final int width;
private final int height;
public Button(String label, int width, int height) {
this.label = label;
this.width = width;
this.height = height;
}
@Override
public void render() {
System.out.println("Button[" + label + "]");
}
@Override
public int getWidth() { return width; }
@Override
public int getHeight() { return height; }
}
public class TextField implements UIComponent {
private final int width;
private final int height;
public TextField(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public void render() {
System.out.println("TextField[" + width + "x" + height + "]");
}
@Override
public int getWidth() { return width; }
@Override
public int getHeight() { return height; }
}
public class Panel implements UIComponent {
private final List<UIComponent> children = new ArrayList<>();
private final String name;
public Panel(String name) {
this.name = name;
}
public void add(UIComponent component) {
children.add(component);
}
@Override
public void render() {
System.out.println("Panel[" + name + "] {");
for (UIComponent child : children) {
System.out.print(" ");
child.render();
}
System.out.println("}");
}
@Override
public int getWidth() {
return children.stream()
.mapToInt(UIComponent::getWidth)
.max()
.orElse(0);
}
@Override
public int getHeight() {
return children.stream()
.mapToInt(UIComponent::getHeight)
.sum();
}
}
// Usage
Panel form = new Panel("LoginForm");
form.add(new TextField(200, 30));
form.add(new TextField(200, 30));
form.add(new Button("Submit", 100, 40));
Panel sidebar = new Panel("Sidebar");
sidebar.add(new Button("Home", 80, 30));
sidebar.add(new Button("Settings", 80, 30));
Panel mainPanel = new Panel("Main");
mainPanel.add(sidebar);
mainPanel.add(form);
mainPanel.render();
How It Works
sequenceDiagram
participant Client
participant Root as Folder "root"
participant Docs as Folder "docs"
participant File1 as File "resume.pdf"
participant File2 as File "letter.docx"
Client->>Root: getSize()
Root->>Docs: getSize()
Docs->>File1: getSize()
File1-->>Docs: 50000
Docs->>File2: getSize()
File2-->>Docs: 25000
Docs-->>Root: 75000
Root-->>Client: 75000
Note over Client,File2: Recursive traversal
Organization Hierarchy Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public interface Employee {
String getName();
double getSalary();
void printHierarchy(String indent);
}
public class Developer implements Employee {
private final String name;
private final double salary;
public Developer(String name, double salary) {
this.name = name;
this.salary = salary;
}
@Override
public String getName() { return name; }
@Override
public double getSalary() { return salary; }
@Override
public void printHierarchy(String indent) {
System.out.println(indent + "👤 " + name + " (Developer) - $" + salary);
}
}
public class Manager implements Employee {
private final String name;
private final double salary;
private final List<Employee> subordinates = new ArrayList<>();
public Manager(String name, double salary) {
this.name = name;
this.salary = salary;
}
public void addSubordinate(Employee employee) {
subordinates.add(employee);
}
@Override
public String getName() { return name; }
@Override
public double getSalary() {
// Total cost includes all subordinates
return salary + subordinates.stream()
.mapToDouble(Employee::getSalary)
.sum();
}
@Override
public void printHierarchy(String indent) {
System.out.println(indent + "👔 " + name + " (Manager) - $" + salary);
for (Employee sub : subordinates) {
sub.printHierarchy(indent + " ");
}
}
}
// Build organization
Manager ceo = new Manager("Alice", 200000);
Manager engineering = new Manager("Bob", 150000);
engineering.addSubordinate(new Developer("Charlie", 100000));
engineering.addSubordinate(new Developer("Diana", 95000));
Manager marketing = new Manager("Eve", 140000);
marketing.addSubordinate(new Developer("Frank", 90000));
ceo.addSubordinate(engineering);
ceo.addSubordinate(marketing);
ceo.printHierarchy("");
System.out.println("Total salary cost: $" + ceo.getSalary());
Common Mistakes
1. Type Checking in Client Code
The point of Composite is uniform treatment:
1
2
3
4
5
6
7
8
9
10
11
12
13
// Wrong - defeats the purpose
for (FileSystemNode node : folder.getChildren()) {
if (node instanceof File) {
System.out.println("File: " + node.getName());
} else if (node instanceof Folder) {
System.out.println("Folder: " + node.getName());
}
}
// Right - uniform treatment
for (FileSystemNode node : folder.getChildren()) {
node.display(""); // Both files and folders respond
}
2. Leaf Methods on Component Interface
Don’t force leaves to implement meaningless methods:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Awkward - leaves must implement container methods
public interface Component {
void operation();
void add(Component c); // Meaningless for leaves
void remove(Component c); // Meaningless for leaves
List<Component> getChildren(); // Meaningless for leaves
}
public class Leaf implements Component {
@Override
public void add(Component c) {
throw new UnsupportedOperationException();
}
// ...
}
// Better - separate interface for containers
public interface Component {
void operation();
}
public interface Container extends Component {
void add(Component c);
void remove(Component c);
List<Component> getChildren();
}
3. Circular References
Avoid adding a node to its own descendant:
1
2
3
4
Folder a = new Folder("a");
Folder b = new Folder("b");
a.add(b);
b.add(a); // Circular! getSize() will infinite loop
Add validation in the add method:
1
2
3
4
5
6
public void add(FileSystemNode node) {
if (this.isDescendantOf(node)) {
throw new IllegalArgumentException("Circular reference");
}
children.add(node);
}
Real-World Examples
Java Swing: JComponent and Container. Every component can contain other components.
DOM: Elements contain other elements. getElementsByTagName works on any element.
File Systems: Directories contain files and directories. du -sh sums sizes recursively.
React Components: Components can contain other components. Props and rendering propagate down.
Related Patterns
Iterator traverses composite structures without exposing internals.
Visitor adds operations to composite structures without modifying them.
Builder can construct complex composite structures step by step.
Decorator uses similar recursive composition but for adding behavior, not structure.
Wrapping Up
Composite creates tree structures where clients treat leaves and containers uniformly. Operations on composites propagate recursively to children.
Use it for file systems, UI trees, organizational hierarchies, and any part-whole relationship. Keep the component interface focused on common operations.
The pattern trades type safety for flexibility. Clients don’t know if they’re handling one item or a thousand. That’s the power and the responsibility.
Further Reading:
- Head First Design Patterns - Chapter 9
- Recursive Data Structures on Wikipedia