Composite Pattern
Learn how the Composite pattern lets a single leaf object and an entire branch of a tree be treated through one identical interface, so operations like a recursive file-system walk fall out naturally with no type-checking.
Learning objectives
- Explain why typing a container to hold one concrete class blocks nesting
- Build a shared component interface that both leaf and composite classes implement
- Trace how a uniform interface produces recursive tree traversal with no special cases
- Recognize when cycles or leaf-only operations need extra care in a Composite design
◆ Story
A restaurant menu lists individual dishes, but also whole sections -- "Appetizers," "Mains" -- and those sections can even contain their own sub-sections. Asked for the total price of everything in the Appetizers section, you genuinely don't care whether you're looking at one dish or an entire nested sub-section -- you just want a total, computed the same way no matter what's actually inside.
This chapter builds that same idea around a file system model: a Folder should be able to contain Files, but critically, a folder should also be able to contain other folders, nested arbitrarily deep, the way a real file system actually works -- and both a file and a folder need to support the same showDetails() operation.
◆ The problem
The obvious starting point is a Folder that holds a List<File> and a showDetails() method that prints itself, then loops over its files printing each one. This works cleanly for a flat folder holding only files.
Now try to put a folder inside another folder -- a completely ordinary thing to want from any real file system. Folder.files is typed List<File>, and there is simply nowhere to put a Folder in it. addFile(Folder) doesn't compile, because File and Folder are two unrelated types, and the container was only ever built to hold one of them.
This isn't a missing feature that a new method can patch -- it's the wrong data type at the foundation, forcing a choice between "a folder holds files" and "a folder holds folders," when a real file system genuinely needs both, simultaneously, in any order.
💻 Code example
class File { private final String name; File(String name) { this.name = name; } void showDetails() { System.out.println("File: " + name); } } class Folder { private final String name; private final List<File> files = new ArrayList<>(); // specifically File, not "anything in a folder" Folder(String name) { this.name = name; } void addFile(File file) { files.add(file); } void showDetails() { System.out.println("Folder: " + name); for (File file : files) file.showDetails(); } } Folder documents = new Folder("Documents"); documents.addFile(new File("file1.txt")); documents.addFile(new File("file2.txt")); documents.showDetails(); // correct, for a flat folder // documents.addFile(new Folder("Subfolder")); -- does not compile at all
Both a file and a folder support the same operation, showDetails(), even though what happens underneath is genuinely different for each. So both should implement a shared interface, and Folder should hold a list of that interface -- not a list of the concrete File class specifically. Because Folder itself would also implement that same interface, a folder becomes a perfectly valid thing to put inside another folder, and nesting falls out with no special case required.
That's the Composite pattern: define one shared interface implemented by both individual "leaf" objects and "composite" objects that hold a collection of other objects implementing that same interface. Callers use the exact same interface either way, with no type-checking anywhere.
File becomes the leaf: it implements FileSystemComponent and holds nothing else -- showDetails() just prints itself, since there's nothing to recurse into. Folder becomes the composite: its one type change, from List<File> to List<FileSystemComponent>, is the entire fix. Folder.showDetails() prints itself first, then calls showDetails() on every child -- and it never checks whether a given child is a File or another Folder. If a child happens to be a Folder, calling showDetails() on it naturally re-enters this exact same method, which is precisely what makes arbitrarily deep nesting work with no extra code.
💻 Code example
interface FileSystemComponent { void showDetails(); } class File implements FileSystemComponent { private final String name; File(String name) { this.name = name; } public void showDetails() { System.out.println("File: " + name); } } class Folder implements FileSystemComponent { private final String name; private final List<FileSystemComponent> components = new ArrayList<>(); // the one type choice that fixes everything Folder(String name) { this.name = name; } void addComponent(FileSystemComponent component) { components.add(component); } // accepts a File OR a Folder public void showDetails() { System.out.println("Folder: " + name); for (FileSystemComponent component : components) { component.showDetails(); // File: prints itself. Folder: recurses into this same method. } } }
Adding a folder inside another folder is now exactly as simple as adding a file -- documents.addComponent(subFolder) works because Folder satisfies FileSystemComponent just like File does, no separate method required.
Calling showDetails() on the top-level folder now performs a depth-first traversal of the entire tree, and showDetails() was never written with "recursion" explicitly in mind -- it fell out naturally from Folder treating every child uniformly through FileSystemComponent. This is the thing the naive List<File> version couldn't do at all.
💻 Code example
Folder documents = new Folder("Documents"); documents.addComponent(new File("file1.txt")); documents.addComponent(new File("file2.txt")); Folder subFolder = new Folder("Subfolder"); subFolder.addComponent(new File("file3.txt")); documents.addComponent(subFolder); // a Folder, added exactly like a File would be documents.showDetails(); // Folder: Documents // File: file1.txt // File: file2.txt // Folder: Subfolder // File: file3.txt <- reached through plain recursion, zero special-case code
▲ Edge case — cycles cause infinite recursion
Nothing in this design stops a folder from directly or indirectly containing itself. If a sub-folder were added back into a folder that's already inside it, showDetails() would recurse forever. Real file systems prevent this structurally; a from-scratch implementation needs to guard against it explicitly whenever cycles are even remotely possible.
▲ Edge case — not every operation makes equal sense on both leaves and composites
showDetails() works cleanly for both a leaf and a composite, but an operation like addComponent() genuinely only makes sense on a Folder -- calling it on a plain File has no sensible meaning. This chapter's design sidesteps the issue by putting addComponent() only on Folder, not on the shared interface. Some Composite implementations instead put it on the shared interface and have leaves throw UnsupportedOperationException, trading a bit of interface segregation for a more uniform interface. Worth choosing deliberately, not by default.
▲ Edge case — Composite only earns its value for a genuine tree
This pattern shines specifically when you have a real part-whole tree structure and want to operate on "one node" and "an entire subtree" through identical code. If your data doesn't actually nest, Composite adds structure you don't need.
◆ Where this shows up
Any file system browser is a real-world Composite: both a file and a folder respond to operations like "get size" or "delete," and a folder's implementation of "get size" simply sums its children's sizes, recursively identical to this chapter's showDetails().
UI frameworks use the same shape for component trees -- a panel containing buttons and other panels, all responding to a shared "render" or "layout" operation regardless of depth. Organizational charts model reporting structures identically: an individual employee and an entire team both respond to "show headcount," recursively, through the exact same interface. Anywhere a domain genuinely nests -- files in folders, components in panels, employees in teams -- Composite is the pattern that lets code operate on any node in that tree without caring how deep it is or what it contains.
Q: What does the Composite pattern let you do that a List-based Folder can't? A: Nest a folder inside another folder -- because Folder holds a list of the shared FileSystemComponent interface rather than the concrete File class, a Folder is itself a valid thing to add to another Folder.
Q: Why doesn't Folder.showDetails() need an explicit check for whether a child is a File or a Folder? A: Both implement the same FileSystemComponent interface and both provide showDetails() -- calling it works identically either way, and if the child happens to be a Folder, the call naturally recurses into the same method.
Q: What's the signal that a problem is a genuine fit for Composite? A: A real part-whole tree structure where you want to operate on one node and an entire subtree through identical code -- file systems and UI component trees are classic examples.
Q: What real bug can an unguarded Composite structure have? A: Infinite recursion, if a composite ends up directly or indirectly containing itself -- nothing in the basic pattern prevents a cycle from being constructed.
Q: Why might addComponent() be placed only on Folder rather than on the shared interface? A: Because it has no sensible meaning on a File -- putting it only where it's genuinely applicable respects interface segregation, at the cost of a slightly less uniform interface than putting it on both and having File throw an exception.
Want a visual for this concept?
Generate a diagram tailored to “Composite Pattern” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →