StudyDSA logoStudyDSA

Command Palette

Search for a command to run...

Sign InSign Up
Sign Up

Data StructuresAlgorithmsBig-O NotationDesign PatternsSystem DesignMachine LearningPhysicsRoboticsAI Research

Factory MethodAbstract FactoryBuilderPrototypeSingletonAdapterBridgeCompositeDecoratorFacadeFlyweightProxyChain of ResponsibilityCommandIteratorMediatorMementoObserverStateStrategyTemplate MethodVisitor

Definition
StudyDSA

Where complexity meets clarity.
By Armas Zarra.

Topics

  • Data Structures
  • Algorithms
  • Big-O Notation
  • Robotics
  • AI Research
  • Machine Learning

Practice

  • Blind 75
  • LeetCode 75
  • NeetCode 150

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Armas Films LLC

Design PatternsComposite

Composite

A structural pattern that arranges objects into trees where single items and whole groups answer the same calls, so clients never write a tree walk.

Definition

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.

The Branch Every Caller Repeats

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.

One Interface, Any Depth

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.

Who Does What

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.

Composite: Step-by-Step

Installing the pattern is four moves:

1. Write the Question Both Kinds Can Answer

The Component interface (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.

2. Let Leaves Answer for Themselves

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.

3. Let Containers Answer by Asking Their Children

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.

4. Hand Clients the Root and Nothing Else

Client code holds one 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.
Without Inheritance: Go and Rust

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.

Isn't This Just Recursion?

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 Uniformity Tax

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.

Best Practices

The structure is three small classes, and these five details keep the tree standing:


Declaring 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.

Check Yourself

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.

the_type_check.py

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))  # 250

composite.py

from 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