June 17, 2026 in patterns5 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 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.
To build this, we need three specific roles:
getSize() and getName()) for both items and containers.File).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
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:
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.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.
if/else chains with polymorphic method calls defined in a shared interface.add() or remove()) to your base interface, as this can force leaf objects to implement meaningless behavior.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