Ask a file its size and it knows. Ask a folder and the honest answer is a recursive walk over everything inside, yet both questions are the same question, and code that treats them differently ends up repeating the same type-check in every function that touches the tree. Composite is the Gang of Four structural pattern that composes objects into tree structures and lets clients treat single objects and whole groups through one interface, the part-whole arrangement the catalog describes as treating individual objects and compositions uniformly.
A composite is really just recursion stored as data. The tree shape lives in the objects themselves, a folder holding entries that may be folders holding entries, so the recursive walk that callers used to write by hand becomes a method that calls the same method on smaller pieces. Clients stop writing loops and start asking questions.
Here's life without the pattern. File and Folder share no interface, so total_size() opens by asking which kind it received, handles one case directly, and hand-rolls the recursion for the other. It works, and the cost is invisible until you notice this same branch-and-recurse boilerplate has to be rebuilt inside every function that ever walks the tree, from total_size to search to rendering:
Adding a third kind of node, say a symlink, means finding every one of those functions and teaching its branch a new case, and the compiler only helps in the languages whose tabs use a tagged union. In the dynamically typed tabs, a missed branch is a runtime surprise. The deeper issue is where the knowledge lives: every caller owns a copy of the tree's shape, when the tree itself is the one thing that already knows it.
The composite version gives both kinds one voice. Entry declares size(), File answers with its byte count, and Folder answers by summing its children's answers, whatever kind each child happens to be. The example builds a report folder holding a 120 byte draft and an images subfolder with files of 80 and 50 bytes. Before reading past the code, predict what report.size() returns and count how many recursive calls the client wrote to get it:
The answers are 250 and zero. The client's entire traversal is one method call, while the recursion happens inside Folder.size(): the report folder asks its two entries, the draft file answers 120 directly, the images folder asks its own children and returns 80 + 50 = 130, and the top level sums to 250. The last line of the example is the pattern's quiet punchline, a bare file answering the identical call with no tree in sight.
Three roles. The Component (Entry) is the shared interface declaring the operations that make sense at every level. The Leaf (File) implements them directly and holds no children, making it the recursion's base case. The Composite (Folder) holds a list of Components and implements each operation by delegating to its children and combining the results. Clients hold a Component reference and stay deliberately ignorant of which role is behind it.
The arrangement works like asking a manager for their headcount. The manager doesn't know the number off-hand, they ask each direct report, individual contributors answer one, sub-managers relay the question downward, and the totals roll back up. The analogy carries a limit worth stating: nothing in this scheme remembers anything, so asking twice walks the entire org chart twice. The pattern buys uniformity, not efficiency, and big trees with hot aggregate queries need caching bolted on deliberately.
Installing the pattern is four moves:
Entry) declares what callers actually want to ask, like size(). If a question only makes sense for containers, it doesn't belong here, and forcing it in is the pattern's classic sore spot.File.size() returns its own byte count and recurses into nothing. Leaves are the base case of the structure, which means the tree's recursion terminates in their one-line method bodies.Folder.size() sums entry.size() over its children without knowing which are files and which are folders. Dispatch sorts that out per child, which is the entire traversal logic of the pattern.Entry and calls size() on it, whether that entry is a lone file or a tree of 10,000 nodes. Depth, counts, and kinds all become the structure's private business.Go needs nothing special: Entry is an interface, File satisfies it with one method, and Folder holds a slice of Entry values it loops over. The before-and-after contrast is sharper in Go than anywhere else, because the untyped version had to take any and type-assert its way through the tree.
Rust offers two genuinely different spellings, and the choice is instructive. The trait-object version in the code block, Vec<Box<dyn Entry>>, keeps the set of node kinds open, so downstream crates can add new Entry types forever. The enum version from the before block, enum Node { File, Folder } with a match, closes the set but makes every traversal exhaustively checked: add a third variant and the compiler lists every match that needs updating. Open extension or checked completeness, and Rust makes you say which one you mean.
You might think Composite is recursion with extra ceremony, and the size computation really is the same arithmetic the hand-rolled version did. What moved is the recursion's home. In the before code, every function that walks the tree owns a copy of the branching and the recursive calls. In the composite, the recursion lives in Folder.size() once, and clients contain none of it, which means a new node kind is one new class instead of an edit to every traversal in the codebase. Composite is what it looks like when recursion becomes a type structure instead of a function's private habit.
The lookalike worth separating is Decorator, which shares the implement-and-hold shape. Arity tells them apart: a decorator wraps exactly one component to layer behavior onto it, while a composite holds many children to aggregate over them. One is a chain, the other is a tree, and the confusion mostly evaporates once you ask how many things the wrapper is holding.
The pattern's sore spot is a real tradeoff the GoF acknowledged: transparency versus safety. Putting add() and remove() on the Component means clients never need to know what they're holding, and also means those methods exist on File, where the only honest implementation throws. Keeping them on Folder is safer and costs clients an occasional downcast at build time. The code block chose safety, and that's the right default until a use case argues otherwise.
The overuse failure is flatter than that: trees where no tree exists. Data that's genuinely a flat list gains nothing from Component, Leaf, and Composite machinery except indirection, and a structure with exactly one level of nesting is usually happier as a list of lists. The pattern earns its keep when depth is unbounded and unknown, which is precisely when hand-rolled traversal code rots fastest.
The structure is three small classes, and these five details keep the tree standing:
add() on the Component lets clients treat everything identically, but forces File.add() to throw at runtime. Declaring it on the Composite only is the safe default: a compile error beats an UnsupportedOperationException in production.Two questions before you go. First: the client's total-size computation contains no loop, no type check, and no recursive call. Name the two places that work went. Second: Composite and Decorator both implement an interface while holding objects of that same interface, so what structural fact separates them, and what does each do with it? Both answers are on this page.
Then go spot it in the wild, which for this pattern barely requires leaving your editor. React's docs model every UI as a render tree of components nesting components, leaf components and all, which is this page's diagram wearing JSX. The filesystem the example mimicked is the pattern too, as is every scene graph, every org chart, and every nested comment thread. Last, grep your codebase for a function that branches on "is this a group or a single item". That branch is a Component interface waiting to be declared, and the function around it is living in the before block.
class File:
def __init__(self, name: str, size_bytes: int):
self.name = name
self.size_bytes = size_bytes
class Folder:
def __init__(self, name: str, entries: list):
self.name = name
self.entries = entries
def total_size(entry) -> int:
# Every traversal re-asks the same question: which kind are you?
if isinstance(entry, File):
return entry.size_bytes
total = 0
for child in entry.entries:
total += total_size(child) # and recurses by hand
return total
# Example usage
report = Folder("report", [
File("draft.txt", 120),
Folder("images", [File("chart.png", 80), File("logo.svg", 50)]),
])
print(total_size(report)) # 250from abc import ABC, abstractmethod
class Entry(ABC): # Component: one interface for files AND folders
@abstractmethod
def size(self) -> int: ...
class File(Entry): # Leaf: answers for itself
def __init__(self, name: str, size_bytes: int):
self.name = name
self.size_bytes = size_bytes
def size(self) -> int:
return self.size_bytes
class Folder(Entry): # Composite: answers by asking its children
def __init__(self, name: str):
self.name = name
self.entries: list[Entry] = []
def add(self, entry: Entry) -> "Folder":
self.entries.append(entry)
return self
def size(self) -> int:
# The recursion lives here, written once, invisible to callers
return sum(entry.size() for entry in self.entries)
# Example usage
images = Folder("images").add(File("chart.png", 80)).add(File("logo.svg", 50))
report = Folder("report").add(File("draft.txt", 120)).add(images)
print(report.size()) # 250
print(File("draft.txt", 120).size()) # 120: same call, no tree needed