Composite
Composes objects into tree structures to represent part-whole hierarchies, letting the client treat individual objects and compositions uniformly through a common interface.
Intent
Compose objects into part-whole trees and let client code treat an individual object (leaf) and a composition of objects (composite node) in exactly the same way — through a common interface.
Cataloged by the GoF (1994) as a structural pattern, the Composite is
the elegant answer to hierarchical structures where the logic applied
to a single element must propagate recursively through the entire
tree. The size() operation on a file returns its fixed
size; the same operation on a directory sums the sizes of all its
children — but the code calling size() doesn't need to
tell the two apart.
Problem
Imagine a file system. It has files — simple objects with a name and a fixed size — and directories, which contain other files and directories. You want to compute the total size of any item: either a single file or an entire directory (recursively summing everything inside it).
Without Composite, the client code needs to distinguish the two cases at every turn:
- If it's a file, return its size directly.
- If it's a directory, iterate over the children, check whether each child is a file or a directory, and recurse if needed.
This results in conditionals scattered throughout the client code — and every time you add a new node type (e.g.: symbolic link), you need to update all of those spots. Composite eliminates this problem: leaves and composites implement the same interface; the recursion is encapsulated inside the composite.
Transparency vs Safety
The GoF describes two approaches for where to place the
child-management methods (add, remove):
- Transparency (in the Component interface): both leaves and composites declare the management methods. The client treats everything uniformly — but leaves need to implement methods that make no sense for them (usually throwing an exception or doing nothing), which violates the Liskov Substitution Principle.
-
Safety (only in the Composite): only the composite
exposes
addandremove. A client that needs to manage children must obtain a reference typed asDirectory, not asComponent. Some uniformity is lost, but the code remains safe and correct at compile time. This is the preferable approach in TypeScript and PHP.
The examples below adopt the safety approach: add exists
only on Directory (the Composite), not on the
Component interface.
Solution
The Composite organizes the code into three participants:
-
Component (interface): the contract shared by
leaves and composites. Defines the operations the client can call
uniformly. E.g.:
Componentinterface withname(): stringandsize(): number. -
Leaf: implements Component and has no children.
Represents the primitive object in the hierarchy. E.g.:
File— returns its size directly. -
Composite: implements Component and keeps a list of
children of type Component. Implements the operations by recursing
into the children. E.g.:
Directory— sums the size of all children, which can be other directories or files.
The key to the pattern is that the Composite stores references of
type Component (the interface), not the concrete type.
This allows mixing leaves and other composites in the same children
list, creating trees of arbitrary depth.
Structure
«interface» Component
┌──────────────────┐
│ + name(): string │
│ + size(): number │
└──────────────────┘
▲ ▲
│ implements │ implements
File Directory
(Leaf) (Composite)
┌──────────────────┐ ┌───────────────────────────┐
│ - _name: string │ │ - children: Component[] │
│ - _size: number │ │ + add(c: Component): this │
│ + name(): string │ │ + name(): string │
│ + size(): number │ │ + size(): number │
└──────────────────┘ │ → sums children sizes │
└───────────────────────────┘
│ children
┌───────┴──────┐
▼ ▼
File Directory
(leaf) (can contain more children)
Example tree:
root/ ← Directory
├── src/ ← Directory
│ ├── main.ts ← File (1200 bytes)
│ └── util.ts ← File (800 bytes)
├── dist/ ← Directory
│ └── main.js ← File (3500 bytes)
└── package.json ← File (400 bytes)
root.size() == 5900 (recursive sum of everything)
src.size() == 2000
dist.size() == 3500
Code examples
Example 1 — File system with recursive size calculation
The client calls size() on any Component
without knowing whether it's dealing with a single file or an entire
directory. The recursion is encapsulated inside Directory.
// ── Component — interface common to leaves and composites ────
interface Component {
name(): string;
size(): number; // in bytes
}
// ── Leaf — object with no children ────────────────────────────
class File implements Component {
constructor(
private readonly _name: string,
private readonly _size: number
) {}
name(): string { return this._name; }
size(): number { return this._size; }
}
// ── Composite — aggregates and recurses into children ─────────
class Directory implements Component {
private readonly children: Component[] = [];
constructor(private readonly _name: string) {}
name(): string { return this._name; }
add(component: Component): this {
this.children.push(component);
return this; // fluent — allows chaining calls
}
size(): number {
// Recurses into children — doesn't distinguish File from Directory
return this.children.reduce((acc, child) => acc + child.size(), 0);
}
list(level = 0): void {
const p = " ".repeat(level);
console.log(`${p}[DIR] ${this._name}/ (${this.size()} bytes)`);
for (const child of this.children) {
if (child instanceof Directory) {
child.list(level + 1);
} else {
console.log(`${p} [FILE] ${child.name()} (${child.size()} bytes)`);
}
}
}
}
// ── Client code — operates on Component uniformly ─────────────
function displaySize(component: Component): void {
console.log(`"${component.name()}" takes up ${component.size()} bytes`);
}
// ── Assembling the tree ────────────────────────────────────────
const root = new Directory("root");
const src = new Directory("src");
const dist = new Directory("dist");
src.add(new File("main.ts", 1200))
.add(new File("util.ts", 800));
dist.add(new File("main.js", 3500));
root.add(src)
.add(dist)
.add(new File("package.json", 400));
root.list();
// [DIR] root/ (5900 bytes)
// [DIR] src/ (2000 bytes)
// [FILE] main.ts (1200 bytes)
// [FILE] util.ts (800 bytes)
// [DIR] dist/ (3500 bytes)
// [FILE] main.js (3500 bytes)
// [FILE] package.json (400 bytes)
// Same call — without distinguishing leaf from composite:
displaySize(new File("readme.md", 200));
// "readme.md" takes up 200 bytes
displaySize(root);
// "root" takes up 5900 bytes
<?php
// ── Component — interface common to leaves and composites ────
interface Component
{
public function name(): string;
public function size(): int; // in bytes
}
// ── Leaf — object with no children ────────────────────────────
class File implements Component
{
public function __construct(
private readonly string $name,
private readonly int $size
) {}
public function name(): string { return $this->name; }
public function size(): int { return $this->size; }
}
// ── Composite — aggregates and recurses into children ─────────
class Directory implements Component
{
/** @var Component[] */
private array $children = [];
public function __construct(private readonly string $name) {}
public function name(): string { return $this->name; }
public function add(Component $component): static
{
$this->children[] = $component;
return $this; // fluent — allows chaining calls
}
public function size(): int
{
// Recurses into children — doesn't distinguish File from Directory
return array_sum(array_map(
fn(Component $child) => $child->size(),
$this->children
));
}
public function list(int $level = 0): void
{
$p = str_repeat(' ', $level);
echo "{$p}[DIR] {$this->name}/ ({$this->size()} bytes)" . PHP_EOL;
foreach ($this->children as $child) {
if ($child instanceof Directory) {
$child->list($level + 1);
} else {
echo "{$p} [FILE] {$child->name()} ({$child->size()} bytes)" . PHP_EOL;
}
}
}
}
// ── Client code — operates on Component uniformly ─────────────
function displaySize(Component $component): void
{
echo "\"{$component->name()}\" takes up {$component->size()} bytes" . PHP_EOL;
}
// ── Assembling the tree ────────────────────────────────────────
$root = new Directory('root');
$src = new Directory('src');
$dist = new Directory('dist');
$src->add(new File('main.ts', 1200))
->add(new File('util.ts', 800));
$dist->add(new File('main.js', 3500));
$root->add($src)
->add($dist)
->add(new File('package.json', 400));
$root->list();
// [DIR] root/ (5900 bytes)
// [DIR] src/ (2000 bytes)
// [FILE] main.ts (1200 bytes)
// [FILE] util.ts (800 bytes)
// [DIR] dist/ (3500 bytes)
// [FILE] main.js (3500 bytes)
// [FILE] package.json (400 bytes)
displaySize(new File('readme.md', 200));
// "readme.md" takes up 200 bytes
displaySize($root);
// "root" takes up 5900 bytes
Example 2 — Org chart with recursive payroll calculation
The same pattern applies to any part-whole hierarchy. An
Employee (leaf) has a fixed salary; a
Department (composite) sums the salaries of all its
members, which can be employees or other nested departments.
// ── Component ─────────────────────────────────────────────────
interface OrgUnit {
title(): string;
totalSalary(): number; // in cents
}
// ── Leaf ──────────────────────────────────────────────────────
class Employee implements OrgUnit {
constructor(
private readonly _title: string,
private readonly _salary: number
) {}
title(): string { return this._title; }
totalSalary(): number { return this._salary; }
}
// ── Composite ─────────────────────────────────────────────────
class Department implements OrgUnit {
private readonly members: OrgUnit[] = [];
constructor(private readonly _title: string) {}
title(): string { return this._title; }
add(member: OrgUnit): this {
this.members.push(member);
return this;
}
totalSalary(): number {
return this.members.reduce((acc, m) => acc + m.totalSalary(), 0);
}
}
// ── Usage ───────────────────────────────────────────────────────
const engineering = new Department("Engineering");
engineering
.add(new Employee("Alice", 1200000)) // $12,000
.add(new Employee("Bruno", 950000)); // $ 9,500
const design = new Department("Design");
design.add(new Employee("Carla", 800000)); // $8,000
const company = new Department("Company");
company
.add(engineering)
.add(design)
.add(new Employee("Diana (CEO)", 2500000)); // $25,000
// The client operates on OrgUnit — doesn't distinguish leaf from composite
const items: OrgUnit[] = [engineering, design, company];
for (const item of items) {
const value = (item.totalSalary() / 100).toFixed(2);
console.log(`${item.title()}: $${value}`);
}
// Engineering: $21500.00 (12000 + 9500)
// Design: $8000.00
// Company: $54500.00 (21500 + 8000 + 25000 ✓)
<?php
// ── Component ─────────────────────────────────────────────────
interface OrgUnit
{
public function title(): string;
public function totalSalary(): int; // in cents
}
// ── Leaf ──────────────────────────────────────────────────────
class Employee implements OrgUnit
{
public function __construct(
private readonly string $title,
private readonly int $salary
) {}
public function title(): string { return $this->title; }
public function totalSalary(): int { return $this->salary; }
}
// ── Composite ─────────────────────────────────────────────────
class Department implements OrgUnit
{
/** @var OrgUnit[] */
private array $members = [];
public function __construct(private readonly string $title) {}
public function title(): string { return $this->title; }
public function add(OrgUnit $member): static
{
$this->members[] = $member;
return $this;
}
public function totalSalary(): int
{
return array_sum(array_map(
fn(OrgUnit $m) => $m->totalSalary(),
$this->members
));
}
}
// ── Usage ───────────────────────────────────────────────────────
$engineering = new Department('Engineering');
$engineering
->add(new Employee('Alice', 1200000)) // $12,000
->add(new Employee('Bruno', 950000)); // $ 9,500
$design = new Department('Design');
$design->add(new Employee('Carla', 800000)); // $8,000
$company = new Department('Company');
$company
->add($engineering)
->add($design)
->add(new Employee('Diana (CEO)', 2500000)); // $25,000
// The client operates on OrgUnit — doesn't distinguish leaf from composite
$items = [$engineering, $design, $company];
foreach ($items as $item) {
printf("%s: $%.2f\n", $item->title(), $item->totalSalary() / 100);
}
// Engineering: $21500.00 (12000 + 9500)
// Design: $8000.00
// Company: $54500.00 (21500 + 8000 + 25000 ✓)
When to use
- When the structure is naturally hierarchical (part-whole): file systems, navigation menus, org charts, arithmetic expression trees, nested UI components. If the structure is a tree, Composite is the natural choice.
- When the client must treat simple and composite objects uniformly: if the code that walks the hierarchy shouldn't care whether the current node is a leaf or a composite, use Composite.
- When operations need to propagate recursively: computing totals, rendering, serializing, applying permissions — any operation that needs to visit every node in the tree.
When to avoid
- When the hierarchy is flat or fixed: if the objects don't form a real tree (e.g.: a list of same-level items), the structural overhead of Composite isn't justified.
- When different types require very distinct interfaces: if leaf and composite need very different methods, forcing an inflated common interface creates more problems than it solves.
- When performance is critical in very deep trees: Composite's recursion walks every node. In extremely deep trees, the call overhead can be significant — consider caching intermediate results or an iterative traversal.
Pros and cons
Pros
- Eliminates conditionals scattered in the client to distinguish leaf from composite — polymorphism handles that.
- Open/Closed Principle: new leaf or composite types can be added without changing the client code or the other nodes.
- Allows building structures of arbitrary depth with the same set of classes.
- The operation propagates naturally through the tree — no traversal code in the client.
Cons
- Can be hard to restrict which component types can be children of which composites — the common interface doesn't express those restrictions.
- The transparency approach (add/remove methods in the Component interface) violates the Liskov Substitution Principle when applied to leaves.
- Debugging deep trees can be laborious — tracing the origin of a recursive value requires walking the whole chain.
Common pitfalls
1. Inflated interface — management methods on leaves
The temptation to put add() and remove() in
the Component interface (transparency approach) is real:
the client gains total uniformity. The price is that File
needs to implement those methods even though it has no children. The
idiomatic practice in TypeScript and PHP is to keep them only on
Directory (safety approach). When the client needs to
call add(), it already knows it's dealing with a
composite — the type system confirms this at compile time.
2. Cycles in the tree
Warning: if a node is added as a child of itself — or
as a descendant of one of its own descendants — the recursion in
size() will enter an infinite loop. Composite doesn't
prevent cycles by default. If the domain allows this possibility
(e.g.: symbolic links in real file systems), implement cycle
detection: keep a Set of already-visited nodes during
the recursion and abort if a repeated one is found.
3. Confusing Composite with Decorator
Both use recursive composition and implement the same interface as the object they wrap. The difference lies in intent and cardinality: the Decorator wraps exactly one object and adds responsibility around its calls. The Composite aggregates N children and its primary operation is to accumulate or propagate the result through the children. If you have a "wrapper around a single object" that adds behavior, it's a Decorator. If you have a node that aggregates and delegates to multiple children, it's a Composite.
4. Missing caching in large trees
Every call to size() on a root directory walks the
entire tree. If the tree is large and immutable (or rarely changes),
compute and cache the result in the composite, invalidating it only
when a child is added or removed. Without this care, repeated
operations on the root have O(n) cost on every call.
Related patterns
The Composite is frequently combined with or compared to other patterns that also deal with composition and traversal of structures:
The Decorator is the pattern with the greatest structural similarity: both use recursive composition with a common interface. The difference lies in cardinality and intent — Decorator wraps a single object to add responsibility; Composite aggregates N children to propagate operations. The Proxy also controls access to a single object and can be applied to nodes of a Composite tree to, for example, load them on demand (lazy loading of subtrees). The Visitor is the natural complement to Composite: it separates the traversal and processing logic of the nodes from the structure itself, allowing new operations to be added without modifying the node classes. The Iterator can be used to walk the tree uniformly without exposing its internal structure.