Structural Pattern (GoF)

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 add and remove. A client that needs to manage children must obtain a reference typed as Directory, not as Component. 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:

  1. Component (interface): the contract shared by leaves and composites. Defines the operations the client can call uniformly. E.g.: Component interface with name(): string and size(): number.
  2. Leaf: implements Component and has no children. Represents the primitive object in the hierarchy. E.g.: File — returns its size directly.
  3. 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

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 ✓)

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.