Skip to content

Composite

Treating a single thing and a group of things identically — the pattern behind file trees and permission groups, and where the uniformity breaks.

Intermediate18 minUpdated 8 August 2026

Composite lets a client treat an individual object and a collection of objects the same way. Every file system, organisation chart, nested menu, arithmetic expression and UI tree you have worked with is one.

The pattern is easy to recognise and easy to misapply, because the uniformity it promises is real for some operations and a lie for others, and the design lives or dies on how you handle the operations where it is a lie.

The problem it removes

Without Composite, every caller branches:

function totalSize(node) {
  if (node.type === 'file') return node.bytes;
  if (node.type === 'folder') {
    return node.children.reduce((sum, c) => sum + totalSize(c), 0);
  }
}

Every operation over the tree repeats that branch — copy, delete, permissions, search — and adding a third node type means finding all of them.

class FileNode {
  constructor(name, bytes) { this.name = name; this.bytes = bytes; }
  size() { return this.bytes; }
}

class FolderNode {
  constructor(name, children = []) { this.name = name; this.children = children; }
  size() { return this.children.reduce((sum, c) => sum + c.size(), 0); }
}
classDiagram
    class Node {
        <<interface>>
        +size() int
        +name() string
    }
    class FileNode {
        -bytes: int
        +size() int
    }
    class FolderNode {
        -children: Node[]
        +size() int
        +add(Node) void
    }
    Node <|.. FileNode
    Node <|.. FolderNode
    FolderNode o-- "0..*" Node

The recursion is now in the type rather than in every caller. root.size() works on a file, a folder, or a tree twelve levels deep, and no caller knows which.

Where the uniformity breaks

The catalogue presents a choice that is really the pattern's central design decision.

Transparent Composite puts child-management operations — add, remove, children — on the shared interface. Every node has them; leaves throw or ignore. Uniformity is complete, and it is achieved by giving leaves methods that are lies.

Safe Composite keeps child management on the composite only. Leaves genuinely cannot have children, so the type system tells the truth — and callers that need to add a child must now know whether they hold a composite, which is exactly the branch the pattern was meant to remove.

There is no free choice here, and picking by taste is how this goes wrong. The useful reframing: operations that read the tree can be uniform; operations that modify its structure usually cannot. So put reads on the shared interface and structural mutation on the composite, and accept that code building trees is different from code walking them. That division matches how the code is actually written — building happens in a few places, walking happens everywhere.

Recursion is the cost

Composite makes tree operations elegant and hides three real costs.

Depth. A recursive size() on a deeply nested tree can exhaust the stack. User-supplied structures — a category tree an admin can nest arbitrarily — need a depth limit at construction or an iterative traversal.

Cycles. A folder containing itself, directly or through a chain, turns any traversal into an infinite loop. If the structure is editable by users or assembled from a database, the invariant "this is a tree, not a graph" must be enforced, not assumed. The cheapest enforcement is at the point of adding a child: refuse if the new child is an ancestor.

Repeated traversal. size() walks everything, every call. Displaying a folder listing with sizes for 200 entries walks the whole subtree 200 times. Either cache and invalidate upwards on change, or compute once and pass the results down.

Genuine uses beyond file trees

Permission and group membership. A group contains users and other groups; "can this principal do X" is one recursive call. The cycle problem is acute here — group A containing group B containing group A is a common data-entry mistake, and the failure is a hung request.

Expression trees. A number and an operation over sub-expressions share an evaluate(). This is the shape most query builders and rules engines have inside.

UI component trees. Render, measure and layout are uniform across a button and a panel of buttons.

Order lines and bundles. A product bundle containing products and other bundles, where price and weight are recursive.

The common signal: a part–whole hierarchy where the whole supports the same questions as the part.

When not to use it

When the operations genuinely differ. If half your code must branch on leaf versus composite anyway, the uniformity is fictional and the interface is hiding a distinction that matters.

When it is not a hierarchy. Composite is for trees. A general graph needs different traversal, and forcing the tree interface onto it produces the cycle bugs above.

When the tree is two levels deep and fixed. An order with lines does not need Composite. Recursion for a structure that cannot nest is complexity with no payoff.

Check your understanding

Common mistakes

Assuming a tree when the data is a graph. Cycles turn every traversal into a hang.

No depth limit on user-editable structures. Arbitrary nesting meets recursive traversal and exhausts the stack.

Recomputing aggregates per call. size() looks like a field and costs a traversal.

Choosing transparent Composite for tidiness. Leaf methods that throw move the distinction from the type system to runtime.

Using it for a fixed two-level structure. An order and its lines do not nest, so recursion buys nothing.

Letting the composite interface grow. Every operation added must make sense for both a leaf and a whole subtree, and most operations do not.

Practice

Design a category tree for a catalogue where admins can nest categories arbitrarily and each category shows a product count including descendants. State the invariants you enforce and where.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Separate what must be true of the structure from what must be fast to read.

Show a strong answer

Structural invariants, enforced at write time rather than checked at read time:

  • No cycles. When setting a category's parent, reject it if the proposed parent is the category itself or one of its descendants. Enforcing on write means a traversal can assume termination; enforcing on read means the bad data already exists and every reader must defend against it.
  • A depth cap — something like eight levels. Admins do not need more, and it converts an unbounded recursive walk into a bounded one. Without it, one administrator experimenting can make the catalogue page fail for everyone.
  • A single root or an explicit forest. Leaving this undecided produces orphan categories that appear in no listing and are found months later.

I would enforce all three in the database as well as the application — a check constraint on depth and a trigger or materialised path for ancestry — because admin tools, migrations and bulk imports will not go through the application's validation.

Product counts including descendants — the read requirement, and the part where the naive Composite implementation fails:

  • Do not recurse per row. A category listing showing counts for 50 categories would traverse the tree 50 times.
  • Store a materialised path or ancestor set per category, so "all descendants of X" is a single indexed query and the count for a whole listing is one grouped query. This is the change that makes the feature viable at all.
  • Cache the count on the category and invalidate up the ancestor chain when a product is added, moved or removed. With a materialised path the ancestor chain is already known, so invalidation is cheap.

Where I would keep the Composite shape: in the domain code that walks a loaded tree — rendering a nav menu, computing a breadcrumb, validating a move. Those operate on a small in-memory subtree and benefit from the uniform interface.

Where I would deliberately abandon it: anything that touches the whole catalogue. Recursive object traversal is the wrong tool against a database; set-based queries over a materialised path are the right one. Composite is a modelling pattern for in-memory structures, and treating it as a data-access strategy is how catalogue pages end up taking four seconds.

A rules engine represents conditions as a tree: leaves are comparisons, composites are AND/OR groups, all sharing `evaluate(context)`. A colleague wants to add `explain()` returning a human-readable description, and `optimise()` returning a simplified tree. Evaluate both additions.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Ask, for each, whether it makes equal sense on a leaf and on a whole subtree.

Show a strong answer

explain() fits the pattern well. A comparison explains itself as "amount is over 100"; an AND group explains itself as its children joined by "and". It is a genuine fold over the tree returning the same type at every level, it is uniform in exactly the way Composite promises, and every node has a sensible answer. I would add it — with one caution: explanation text is presentation, and putting it on the domain node couples the rules model to a language and a format. A separate visitor that walks the tree and produces text keeps the node free of that, and lets a second visitor produce a different format later. For one format, the method is fine; for two, use a visitor.

optimise() is a poorer fit and the reason is worth stating precisely. It is not a uniform operation.

  • A leaf cannot optimise. It returns itself, which is a method that exists to satisfy the interface.
  • The interesting rules are not local. Flattening nested ANDs, removing duplicate conditions, short-circuiting an OR containing an always-true — these are properties of a node and its children considered together, and some need the whole tree. A method on each node either sees too little or reaches around the interface to look at descendants' internals.
  • It changes the tree's identity. evaluate and explain answer questions; optimise returns a different tree. Mixing question-answering and transformation on one interface is how the interface starts growing.

What I would do instead: an optimiser that owns the rewrite rules and walks the tree — a visitor, or plain recursive functions over the node types. Rules live together where they can be read as a set, tested independently, and extended without touching every node class. That is also the honest signal that this operation is about the tree rather than of it.

The general test I would apply to any addition to a Composite interface: does a leaf have a real answer, or one that exists only to satisfy the interface? evaluate yes, explain yes, optimise no. The third one belongs outside.

Key takeaways

  • Composite lets clients treat a part and a whole identically, moving recursion from every caller into the type.
  • The transparent-versus-safe choice is a real trade-off: put reads on the shared interface and structural mutation on the composite, since trees are built in few places and walked in many.
  • Enforce tree-ness where edges are created. Cycles turn every traversal into a hang, and user-editable structures produce them routinely.
  • Cap depth for user-supplied nesting, or recursive traversal will exhaust the stack.
  • Recursive aggregates look like field reads and cost a traversal. Compute in one pass or cache with upward invalidation.
  • Composite is an in-memory modelling pattern; against a database, use materialised paths and set-based queries instead.
  • Before adding a method to the interface, ask whether a leaf has a real answer or only one that satisfies the interface.

Further reading

  • Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).
  • Joe Celko, Trees and Hierarchies in SQL for Smarties, second edition (Morgan Kaufmann, 2012) — materialised paths and nested sets.
  • Martin Fowler, Patterns of Enterprise Application Architecture (Addison-Wesley, 2002).
  • Eric Evans, Domain-Driven Design (Addison-Wesley, 2003) — aggregates and the boundaries of a part–whole structure.
  • Alfred Aho, Monica Lam, Ravi Sethi and Jeffrey Ullman, Compilers: Principles, Techniques, and Tools, second edition (Pearson, 2006) — expression trees and traversal.

Check yourself

3 questions in this lesson. Answer each once to complete it.

Practice this

Next lessonBridge
Lessons in this track