skip to content

Walk through the structure of a Composite hierarchy — the participants, where the recursion lives, and how a container computes an aggregate result such as total size or overall validity.

level: middleimportance: must knowfreq 52%

answer

  1. Component / Leaf (base case) / Composite (recursive case)
  2. no self-calling function — recursion via polymorphic dispatch
  3. fold per operation: sum, AND, first-hit, max+1
  4. identity value for the empty container
  5. cycle guard in add; depth vs stack; N+1 on lazy children

basics

~20 s

A Component interface declares the operation. A Leaf implements it directly (base case). A Composite holds a list of Components and implements the operation by calling it on each child and combining the results (recursive case). The client only ever sees Component.

solid answer

~50 s

Three participants plus the client. **Component** declares the operations clients invoke. **Leaf** implements each one directly — it is the recursion's base case and returns a concrete value with no delegation. **Composite** holds a collection typed as Component, and implements each operation by invoking the same operation on every child and folding the results with a combining rule specific to that operation: sum for size, logical AND for validity, first-hit-wins for search, max-plus-one for depth, side-effect-in-order for rendering. Two details matter. First, the combining rule needs an identity for the empty container — 0 for a sum, true for an AND, absent for a max — and getting that wrong is the classic bug. Second, the recursion terminates only because leaves exist and the structure is acyclic; if a container can ever contain an ancestor, every traversal loops forever, so `add` must reject cycles. Depth is bounded by the call stack, so very deep trees may need an explicit stack instead of recursion.

code

pseudocode · 18 lines
pseudocode
interface Node { size(): int; valid(): bool }

class FileNode implements Node {        // base case: no delegation
    bytes: int
    size()  = bytes
    valid() = bytes >= 0
}

class DirNode implements Node {         // recursive case
    children: List<Node>
    size()  = children.fold(0,    (a,c) -> a + c.size())   // identity 0
    valid() = children.fold(true, (a,c) -> a && c.valid()) // identity true

    add(c: Node) {
        require(c !== this && !c.contains(this))  // reject cycles
        children.add(c)
    }
}

go deeper

for a junior

Name the three participants and show the base case (leaf returns a value) versus the recursive case (container loops over children and combines).

for a middle

Add that recursion comes from polymorphic dispatch rather than a self-calling function, and that each operation has its own combining rule plus an empty-container identity.

for a senior

Bring up termination and the cycle guard, tree versus DAG semantics, stack depth on untrusted input, and hidden cost such as N+1 loading.

for a principal

Discuss aggregate caching and invalidation up the parent chain, immutability as the way to make caching sound, iterative traversal for adversarial depth, and when to move operations out into a Visitor.

## The three participants, precisely **Component** — the abstract type (interface or abstract base class). It declares exactly the operations clients want to invoke on *any* node. It is the only type appearing in client code and in the container's child collection. Nothing about children need appear here (see the transparency discussion), but the client operations must. **Leaf** — a node with no children. It implements each operation *terminally*: it computes and returns without calling the operation on anything else. In recursion terms, **Leaf is the base case**. A hierarchy usually has several leaf classes (File, Symlink, Button, Label) and they need not be related to each other beyond implementing Component. **Composite** — a node holding `children: Collection<Component>`. It implements each operation as: invoke the same operation on every child, then fold the results. **Composite is the recursive case.** It may also contribute its own intrinsic value (a directory's own metadata size, a panel's own border). **Client** — code holding `Component` references. It calls `root.size()` and receives a number, with no idea whether one node or a million were visited. ## Where recursion actually lives A point candidates often miss: **there is no recursive function anywhere.** No method calls itself by name. The recursion is *structural* — `Composite.size()` calls `Component.size()`, and because a child may itself be a Composite, control re-enters `Composite.size()` through **polymorphic dispatch** (the runtime selects the implementation from the object's actual class). This is why Composite is sometimes described as "recursion encoded in the type system rather than in a function." The practical consequence: to add a new node kind you write a new class; you never edit an existing traversal. ## The combining rule is per-operation A container is not a generic "loop and sum" machine. Each operation has its own fold: | Operation | Combining rule over children | Empty-container identity | Notes | |---|---|---|---| | `size()` | sum | `0` | May add the container's own overhead | | `price()` | sum, then maybe apply a bundle discount | `0` | Container can transform the aggregate | | `isValid()` | logical AND | `true` (vacuously valid) | Or `false` if your domain forbids empty groups | | `containsX()` | logical OR | `false` | Should short-circuit on the first hit | | `find(name)` | first non-absent result | absent | Order of children determines which match wins | | `depth()` | 1 + max | `1` (or `0`) | `max` has no identity — define explicitly | | `draw()` | side effect on each, in order | no-op | Order is semantically significant (z-order) | | `collectAll()` | concatenate | empty list | Flattening operation | **The empty-container case is where bugs live.** "Are all the items in this order in stock?" over an empty bundle returns `true` by the AND identity — which may be logically right and commercially wrong. Decide it deliberately and test it. Also decide whether the container's own intrinsic value participates: `Directory.size()` returning only the sum of children ignores directory metadata; whether that is correct is a domain question, not a pattern question. ## Termination conditions The recursion terminates only if two things hold: 1. **Leaves exist** on every path — a structure of nothing but containers would recurse until it ran out of children anyway, but a container that adds itself never terminates. 2. **The structure is acyclic.** Composite assumes a **tree**: exactly one root, each node reachable by exactly one path, no node is its own ancestor. If `add` lets you insert an ancestor, `size()` spins forever (or blows the stack). Therefore a robust `add` checks: the child is not this node, and this node is not reachable from the child. If you deliberately allow a node to have multiple parents, you have a **directed acyclic graph (DAG)**, not a tree. That is a valid and sometimes useful choice (shared subtrees save memory, as in a Flyweight-style scene graph), but it changes semantics: a summing aggregate now **double-counts** shared subtrees, a `parent` pointer is no longer well-defined, and traversals that must visit each node once need a **visited set**. ## Depth, stack, and cost Structural recursion consumes one stack frame per level. Trees built from user data (a deeply nested file system, a pathological JSON document, an expression parsed from untrusted input) can exceed the stack and crash the process — a real availability concern when the input is attacker-controlled. Mitigations: enforce a maximum depth on insert/parse, or replace the recursive walk with an explicit iterator that keeps a stack on the heap (this is essentially adding an Iterator to the Composite). Cost is invisible at the call site: `root.size()` looks like a field access. If children live behind lazy database or network loading, a single innocuous call can trigger one query per node — the **N+1 problem**. Fixes: eager/batched loading of the subtree, or caching the aggregate on the container and invalidating it up the parent chain when a descendant changes. ## Caching aggregates A common optimisation is memoising a container's aggregate (`cachedSize`). Correctness then requires that **any** mutation anywhere below invalidates every ancestor's cache — which requires parent pointers, which in turn requires `add`/`remove` to maintain them, and which is unsafe under concurrent mutation without synchronisation. This is why immutable trees are attractive: an immutable node's aggregate can be computed once at construction and never invalidated. ## Putting it together ``` interface Node { size(): int; valid(): bool } class FileNode implements Node { // base case bytes: int size() = bytes valid() = bytes >= 0 } class DirNode implements Node { // recursive case children: List<Node> size() = children.fold(0, (acc, c) -> acc + c.size()) // identity 0 valid() = children.all(c -> c.valid()) // identity true add(c: Node) { require(c !== this && !c.contains(this)) // cycle guard children.add(c) } } ``` That is the whole pattern: one interface, a terminal implementation, a delegating implementation, and a consciously chosen fold per operation.

  • Your Composite caches its aggregate size. What must happen when a leaf ten levels down changes?
    Every ancestor's cached value must be invalidated, which requires parent pointers maintained correctly by add/remove, and synchronisation if mutation can happen concurrently. Immutable nodes avoid the problem: the aggregate is computed at construction and an edit produces new nodes along the changed path.
  • What changes if a node is allowed to have more than one parent?
    The structure becomes a DAG rather than a tree. Summing aggregates double-count shared subtrees, a single parent pointer is no longer meaningful, and any traversal that must visit each node once needs a visited set. It is a legitimate choice for memory sharing, but the semantics must be redefined deliberately.
  • How do you protect a Composite traversal from stack overflow on very deep trees?
    Either bound depth at insert or parse time, or convert the recursive walk into an iterative one with an explicit heap-allocated stack — effectively giving the Composite an Iterator. This matters most when tree shape comes from untrusted input.
  • Where would you put an operation that needs different behaviour for many node types and is added often, such as export-to-format?
    Consider a Visitor: it keeps the node classes closed while letting you add operations externally. The trade-off reverses — adding operations becomes cheap, adding node types becomes expensive because every visitor must be updated.

saying these in an interview costs you the question

  • Saying "the Composite method calls itself recursively" — no method calls itself by name; recursion arises from polymorphic dispatch on children typed as Component.
  • Assuming every aggregate is a sum; validity is an AND, search is first-hit, depth is max+1, and rendering is an ordered side effect.
  • Ignoring the empty-container result, which is where most Composite bugs hide.
  • Omitting the cycle guard in add — a container that can contain an ancestor makes every traversal hang.
  • Treating shared nodes (multiple parents) as harmless; sums double-count and parent pointers become meaningless.
  • Ignoring that a single call on the root can trigger unbounded work or one database query per node.

context