Intent

Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects (leaves) and compositions of objects (branches) uniformly — the recursion is the design.

The Problem

You're modelling a file system. You have files (with a size in bytes) and folders (which contain other files and folders). You want to:

  • Compute the total size of a folder.
  • List its contents.
  • Search by name.
  • Copy or delete it as a whole.

The first version writes branchy code everywhere:

PHP
public function totalSize(FileSystemNode $node): int
{
    if ($node instanceof File) {
        return $node->size;
    }
    if ($node instanceof Folder) {
        $total = 0;
        foreach ($node->children as $child) {
            $total += $this->totalSize($child);   // recursion only here
        }
        return $total;
    }
    throw new \InvalidArgumentException();
}

Now every operation that walks the tree carries the same if (instanceof File) ... else ... ladder. Add a third kind of node — a symlink — and every operation needs editing again. Worse, the callers of totalSize have to know whether they're holding a file or a folder; the abstraction leaks.

The shape: a tree where the same operation should apply to a leaf and to a branch — and the cleanest way to make that possible is to let both kinds of node share an interface.

The Solution

Composite says: define one interface (FileSystemNode) that both leaf and composite nodes implement. The leaf does the operation directly. The composite does it by recursing into its children and combining their results — but exposes the same method to callers.

PHP
interface FileSystemNode
{
    public function name(): string;
    public function totalSize(): int;
}

final class File implements FileSystemNode
{
    public function __construct(public string $name, public int $size) {}
    public function name():      string { return $this->name; }
    public function totalSize(): int    { return $this->size; }
}

final class Folder implements FileSystemNode
{
    /** @var FileSystemNode[] */
    private array $children = [];

    public function __construct(public string $name) {}
    public function add(FileSystemNode $child): void { $this->children[] = $child; }

    public function name(): string { return $this->name; }
    public function totalSize(): int
    {
        $total = 0;
        foreach ($this->children as $child) $total += $child->totalSize();
        return $total;
    }
}

$folder->totalSize() works whether $folder is the root or a single file's parent — the recursion is hidden. The instanceof check in the caller disappears entirely. Adding a third node type (Symlink) is a new class implementing FileSystemNode — no existing code moves.

Real-World Analogy

An organizational chart. The CEO has direct reports. Some reports are individual contributors — they own no one. Some are managers, with their own reports. Some of those managers manage other managers, who manage other reports. Pick any branch and ask: how many people work here?

The answer takes the same shape regardless of where you stand: an individual contributor counts as one person; a manager counts as themselves plus the answer for each of their reports. You don't need a different question for "branch" versus "leaf" — the same question, recursively answered, works everywhere.

Structure

Composite pattern: a FileSystemNode interface implemented by a File leaf and a Folder composite that holds a list of FileSystemNode children.
Composite: leaf and composite both speak the same interface; composites hold children of the same type.

Three roles you'll see in every Composite implementation:

  • Component — the interface every node implements. Defines the operations that work uniformly on leaves and composites. Here: FileSystemNode.
  • Leaf — a node with no children. Implements the operations directly. Here: File.
  • Composite — a node that holds children (which are themselves Components, leaf or composite). Implements the operations by recursing into the children. Here: Folder.

The defining property: the Composite's children are typed as the Component interface, not as a specific subclass. That's what enables arbitrary nesting — and what lets the same operation work at any depth.

Code Examples

Here's a small file-system Composite in five languages. Notice how the recursion lives in totalSize on the composite, and nowhere else.

interface FileSystemNode {
  name(): string;
  totalSize(): number;
}

class File implements FileSystemNode {
  constructor(private fileName: string, private size: number) {}
  name():      string { return this.fileName; }
  totalSize(): number { return this.size; }
}

class Folder implements FileSystemNode {
  private children: FileSystemNode[] = [];

  constructor(private folderName: string) {}

  add(child: FileSystemNode): void { this.children.push(child); }

  name(): string { return this.folderName; }

  totalSize(): number {
    return this.children.reduce((sum, c) => sum + c.totalSize(), 0);
  }
}

// Use:
const root = new Folder("project");
root.add(new File("README.md", 1_200));
const src = new Folder("src");
src.add(new File("index.ts", 4_800));
src.add(new File("util.ts", 2_400));
root.add(src);

console.log(root.totalSize());   // 8400 — recurses into nested folders.
from abc import ABC, abstractmethod

class FileSystemNode(ABC):
    @abstractmethod
    def name(self):       ...
    @abstractmethod
    def total_size(self): ...

class File(FileSystemNode):
    def __init__(self, name, size):
        self._name, self._size = name, size
    def name(self):       return self._name
    def total_size(self): return self._size

class Folder(FileSystemNode):
    def __init__(self, name):
        self._name = name
        self._children = []
    def add(self, child):
        self._children.append(child)
    def name(self):       return self._name
    def total_size(self): return sum(c.total_size() for c in self._children)

# Use:
root = Folder("project")
root.add(File("README.md", 1_200))
src = Folder("src")
src.add(File("index.py", 4_800))
src.add(File("util.py",  2_400))
root.add(src)

print(root.total_size())   # 8400
public interface FileSystemNode {
    String name();
    long totalSize();
}

public final 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 name()      { return name; }
    @Override public long   totalSize() { return size; }
}

public final 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 child) { children.add(child); }

    @Override public String name() { return name; }

    @Override
    public long totalSize() {
        return children.stream().mapToLong(FileSystemNode::totalSize).sum();
    }
}
<?php

namespace App\Fs;

interface FileSystemNode
{
    public function name():      string;
    public function totalSize(): int;
}

final class File implements FileSystemNode
{
    public function __construct(private string $fileName, private int $size) {}

    public function name():      string { return $this->fileName; }
    public function totalSize(): int    { return $this->size; }
}

final class Folder implements FileSystemNode
{
    /** @var FileSystemNode[] */
    private array $children = [];

    public function __construct(private string $folderName) {}

    public function add(FileSystemNode $child): void { $this->children[] = $child; }

    public function name(): string { return $this->folderName; }

    public function totalSize(): int
    {
        $total = 0;
        foreach ($this->children as $child) {
            $total += $child->totalSize();
        }
        return $total;
    }
}
package fs

type FileSystemNode interface {
    Name()      string
    TotalSize() int64
}

type File struct {
    FileName string
    Size     int64
}

func (f File) Name() string     { return f.FileName }
func (f File) TotalSize() int64 { return f.Size }

type Folder struct {
    FolderName string
    children   []FileSystemNode
}

func NewFolder(name string) *Folder {
    return &Folder{FolderName: name}
}

func (f *Folder) Add(child FileSystemNode) {
    f.children = append(f.children, child)
}

func (f *Folder) Name() string { return f.FolderName }

func (f *Folder) TotalSize() int64 {
    var total int64
    for _, c := range f.children {
        total += c.TotalSize()
    }
    return total
}

The leaf does the work. The composite does the recursion. Callers see one method, regardless of the tree's depth — and that's the entire promise of the pattern.

When to Use It

Reach for Composite when you can answer "yes" to any of these:

  • You have a tree-shaped structure. File systems, UI hierarchies (component → component → component), AST nodes, organisational charts, menu hierarchies, scene graphs in games, BOMs in manufacturing.
  • The same operations apply to single nodes and groups. "Render this," "compute size," "validate," "delete," "search" — uniform regardless of depth.
  • Callers shouldn't care how deep the tree is. They want to ask what the tree contains or what its total is, not navigate the structure themselves.
  • The set of node types is reasonably stable. Adding new node types (a Symlink, a HardLink) means adding new classes implementing the Component interface — easy. Adding new operations across all nodes is what the Visitor pattern handles more cleanly.

If your data is flat (a list, a set), Composite is overkill — a simple aggregate works. The pattern earns its keep when the structure itself is recursive.

Pros and Cons

Pros

  • Clients work with one interface, regardless of whether they hold a leaf or a composite. The instanceof ladder disappears.
  • Adding new node types is a clean addition — no edits to existing nodes or to callers.
  • The recursive structure mirrors the problem domain — easy to explain, easy to read.
  • Operations naturally support arbitrary depth without extra code.

Cons

  • Type safety can suffer. If "add child" only makes sense on composites — not on leaves — you have a choice: put add() in the Component interface (transparent but unsafe), or put it only on Composite (safe but instanceof-y at use sites).
  • Operations that only make sense on one kind of node (e.g., "is this a directory?") feel awkward. Either default-return on leaves or accept that callers will sometimes need to check.
  • Performance can surprise. A single operation on a deep tree fans out into many recursive calls. Most of the time this is fine; for very large trees, profile before assuming.

Pro Tips

  • Decide transparent vs. safe up front. Transparent Composite (Component declares add/remove/etc.) means clients never need to check the type but can call meaningless methods on leaves. Safe Composite (only Composite declares them) means clients sometimes have to check the type but get compile-time correctness. Most modern code prefers transparent — and lets leaves throw or no-op on the meaningless calls.
  • Pair Composite with Iterator. Walking a tree is exactly what iterators do well. A DepthFirstIterator on a Composite is a beautiful pairing.
  • For adding operations across an existing Composite, consider Visitor instead of editing the interface. Adding a new method to Component means editing every existing node class. Visitor lets you add operations as separate classes that traverse the tree.
  • Cache aggregated values when traversal is expensive. If totalSize() is called often and the tree is deep, cache the result on each Composite and invalidate on modification.
  • Don't model degenerate trees as Composites. If your hierarchy is always two levels (parent + children, never grandchildren), the recursion is unused — a regular collection class is simpler.

Relations with Other Patterns

  • Iterator is the natural companion: once you have a tree, you usually want to walk it. Iterator hides whether the walk is depth-first or breadth-first.
  • Decorator has a similar "wrap and delegate" shape but a different intent: Decorator wraps a single object to add behavior; Composite holds many children to compose them.
  • Visitor is the right partner when you want to add new operations across a stable Composite hierarchy without editing every node class.
  • Chain of Responsibility can be implemented over a Composite — a request travels up the parent chain (or down through children) until handled.
  • Flyweight sometimes pairs with Composite when many leaves share state — instead of millions of distinct leaf objects, one shared instance with extrinsic state.

Final Tips

The cleanest Composite I've ever shipped was a permissions tree — a hierarchy where each node was either a single permission (posts.publish) or a group of permissions (posts.*, which expanded into every posts.X). The same userHasPermission() check worked whether you passed it a single permission or a group. New permissions slotted in; new groups slotted in; no caller changed.

That's the deep promise: when the recursion is the model, the code that uses the model gets to ignore the recursion entirely. Reach for Composite when "single or group" should be invisible to the caller — and the structure is genuinely tree-shaped, not just a flat list with extra steps.