Composite
Purpose
Let a client treat one thing and a group of things identically, recursively. The pattern is
correct exactly when the client's operation is genuinely indifferent to the distinction — the
size of a file or a directory, the total of a line or a section, whether a permission is
granted, whether a rule passes.
Everything difficult about Composite comes from the structure rather than the interface: trees
have depth, may acquire cycles, are mutated while being walked, and are serialised. Those are
the failures that reach production; the uniform interface is the easy part.
When it is the answer
The structure is genuinely recursive — a composite can contain
composites, to arbitrary depth
→ Composite.
Clients perform an operation whose meaning is the same for one and
for many (size, total, evaluate, render, matches)
→ Composite.
Examples that fit: ASTs and expression trees, file and document
trees, organisation and permission hierarchies, composite validation
rules and specifications, UI component trees.
When it is not
- The nesting is two levels and fixed. An order with lines is not a composite; it is an
object with a collection. Recursion you will never use costs clarity.
- Leaves cannot honour the operations. If
add, remove or children are meaningless for a
leaf, the "uniform" interface is a lie the leaf pays for by throwing.
- Clients constantly need to know which they hold. Every
instanceof at a call site is
evidence that the operation is not indifferent — model the difference instead of hiding it.
- The children are remote. A uniform interface over local and remote children hides N network
calls behind a loop (
gof-patterns-and-distribution).
- The structure is an unrestricted graph with undefined visit semantics. Composite can
represent DAGs or cyclic graphs only when each operation defines visited-node identity,
duplicate handling and termination. Without those policies, tree-style recursion is unsafe.
Transparent, safe, or sealed
The examples with record/sealed types and exhaustive type-pattern switches target Java 21 without
preview. Inspect the project's compiler release; records/sealed types alone are available on
Java 17, and ordinary classes can express Composite on earlier baselines without an upgrade.
Transparent (GoF's preference)
Component declares add/remove/getChild; Leaf throws
→ uniform type, but the interface promises what leaves cannot do,
and the failure is at runtime
Safe
only Composite declares add/remove; clients downcast to mutate
→ honest types, but clients test and cast
Sealed + pattern matching (closed-world option)
sealed interface Node permits Leaf, Branch
→ the shared operation stays on the interface; structural operations
live on Branch; a switch over the closed set is exhaustive and the
compiler finds every site when a variant is added
The sealed form shifts the trade-off: clients that only evaluate use the interface, while
structural clients switch exhaustively without unchecked casts. In exchange, the hierarchy is
closed and adding a permitted subtype can force changes in exhaustive clients. Prefer it when
closed-world control and compiler-assisted evolution outweigh plugin extensibility
(java-composition-over-inheritance).
Decision rules
IF the tree's depth comes from data you do not control
THEN recursion is a denial-of-service surface. Bound the depth on
construction, and traverse iteratively with an explicit deque.
IF nodes hold parent pointers
THEN bidirectional traversal has cycles. Ensure equals, hashCode, toString,
serializers and walkers do not recursively follow both directions—use identity,
exclude back-references, or track visited nodes.
IF the tree is mutable and may be traversed concurrently
THEN a walk can see a half-applied change or throw
ConcurrentModificationException. Prefer immutable nodes with
structural sharing; if mutable, state the locking.
IF a leaf must implement an operation that has no meaning for it
THEN the interface is wrong. Move that operation to the composite type.
IF the same node instance appears in two places
THEN it is a DAG. Decide whether aggregation is per edge/path or per node identity;
neither is universally correct. Forbid sharing or track visited identity when
the chosen semantics require it.
IF an operation over the tree needs to know each node's concrete type
THEN it is a Visitor or a pattern-matched fold, not a method on
Component (gof-visitor).
IF children are fetched lazily from a database
THEN inspect query counts and required subtree size. Batch, prefetch, page or query the needed
aggregate; neither a query per node nor loading an unbounded whole tree is inevitable
(orm-behavioral-patterns).
Cross-cutting checks
- Concurrency. Nothing about the pattern is thread-safe. The realistic hazards are a
traversal running while a child is added —
ConcurrentModificationException in some
collection implementations, a walk
that silently skips a subtree at worst — and a "total" computed across a mutation, which is
arithmetically consistent with no state the tree ever had. Immutable nodes with a copy-on-write
root reference remove both only when the reachable nodes are deeply immutable, and then make
caching a computed aggregate safe.
- Distribution. A composite is process-local. Where children are references into another
service, the uniform interface turns one call into a fan-out whose latency is the slowest
branch and whose failure semantics are partial (
scatter-gather). Where trees are transmitted,
depth is an attack surface — deeply nested JSON or XML exhausts the parser's stack or the
serialiser's, so a depth limit belongs at the boundary, not in the domain.
- Performance. Per-node object overhead can dominate wide shallow trees of small payloads;
measure layout rather than infer it from node count. Recursive traversal consumes native
thread-stack space and HotSpot generally does not perform tail-call elimination. Where a tree is walked
in a hot path, consider computing an aggregate incrementally at mutation time, or flattening to
an array-backed representation — after measuring (
allocation-profiling).
- Testing. Trees are where property-based tests pay: generate random structures and assert
invariants (total of a branch equals the sum of its children; a walk visits every node once;
depth-limit rejection). Include a degenerate deep chain in the suite — that is the case
production finds and unit tests miss.
Review checklist
References
- Structure and hazards — transparent, safe and sealed
compared with what each costs, iterative traversal, depth bounding, cycles and identity,
equals/hashCode on recursive structures, parent pointers, and the mutation-versus-traversal
rules. Read before implementing a tree that outlives a single method.
- Worked example — an organisational permission tree: the
transparent version and its throwing leaf, the sealed version, an iterative resolver with a
depth bound, caching an aggregate safely under immutability, and the property tests. Read when
implementing.
1---2name: gof-composite3description: Composite in modern Java: treating a leaf and a tree of leaves through one interface, and the hazards that come with a recursive structure. Covers the transparent-versus-safe trade-off and when a sealed interface with exhaustive pattern matching changes that trade-off, unbounded depth and StackOverflowError, cycles introduced by parent pointers and the infinite recursion they cause in equals, hashCode and toString, mutation during traversal, and why a tree whose children live in other services is not this pattern. Use when a part-whole hierarchy is being modelled, when a leaf class is forced to implement add() and throw, when a recursive walk overflows the stack on production data, when nested structures arrive from untrusted input, or when someone proposes Composite for a flat group of items. Does not cover adding operations over a tree (gof-visitor), traversal protocols (gof-iterator), adding behaviour to one object (gof-decorator), or aggregate boundaries in a domain model (domain-logic-organization).4---56# Composite78## Purpose910Let a client treat one thing and a group of things identically, recursively. The pattern is11correct exactly when the client's operation is genuinely indifferent to the distinction — the12size of a file or a directory, the total of a line or a section, whether a permission is13granted, whether a rule passes.1415Everything difficult about Composite comes from the structure rather than the interface: trees16have depth, may acquire cycles, are mutated while being walked, and are serialised. Those are17the failures that reach production; the uniform interface is the easy part.1819## When it is the answer2021```text22The structure is genuinely recursive — a composite can contain23composites, to arbitrary depth24 → Composite.2526Clients perform an operation whose meaning is the same for one and27for many (size, total, evaluate, render, matches)28 → Composite.2930Examples that fit: ASTs and expression trees, file and document31trees, organisation and permission hierarchies, composite validation32rules and specifications, UI component trees.33```3435## When it is not3637- **The nesting is two levels and fixed.** An order with lines is not a composite; it is an38 object with a collection. Recursion you will never use costs clarity.39- **Leaves cannot honour the operations.** If `add`, `remove` or `children` are meaningless for a40 leaf, the "uniform" interface is a lie the leaf pays for by throwing.41- **Clients constantly need to know which they hold.** Every `instanceof` at a call site is42 evidence that the operation is not indifferent — model the difference instead of hiding it.43- **The children are remote.** A uniform interface over local and remote children hides N network44 calls behind a loop (`gof-patterns-and-distribution`).45- **The structure is an unrestricted graph with undefined visit semantics.** Composite can46 represent DAGs or cyclic graphs only when each operation defines visited-node identity,47 duplicate handling and termination. Without those policies, tree-style recursion is unsafe.4849## Transparent, safe, or sealed5051The examples with record/sealed types and exhaustive type-pattern switches target Java 21 without52preview. Inspect the project's compiler release; records/sealed types alone are available on53Java 17, and ordinary classes can express Composite on earlier baselines without an upgrade.5455```text56Transparent (GoF's preference)57 Component declares add/remove/getChild; Leaf throws58 → uniform type, but the interface promises what leaves cannot do,59 and the failure is at runtime6061Safe62 only Composite declares add/remove; clients downcast to mutate63 → honest types, but clients test and cast6465Sealed + pattern matching (closed-world option)66 sealed interface Node permits Leaf, Branch67 → the shared operation stays on the interface; structural operations68 live on Branch; a switch over the closed set is exhaustive and the69 compiler finds every site when a variant is added70```7172The sealed form shifts the trade-off: clients that only evaluate use the interface, while73structural clients switch exhaustively without unchecked casts. In exchange, the hierarchy is74closed and adding a permitted subtype can force changes in exhaustive clients. Prefer it when75closed-world control and compiler-assisted evolution outweigh plugin extensibility76(`java-composition-over-inheritance`).7778## Decision rules7980```text81IF the tree's depth comes from data you do not control82THEN recursion is a denial-of-service surface. Bound the depth on83 construction, and traverse iteratively with an explicit deque.8485IF nodes hold parent pointers86THEN bidirectional traversal has cycles. Ensure equals, hashCode, toString,87 serializers and walkers do not recursively follow both directions—use identity,88 exclude back-references, or track visited nodes.8990IF the tree is mutable and may be traversed concurrently91THEN a walk can see a half-applied change or throw92 ConcurrentModificationException. Prefer immutable nodes with93 structural sharing; if mutable, state the locking.9495IF a leaf must implement an operation that has no meaning for it96THEN the interface is wrong. Move that operation to the composite type.9798IF the same node instance appears in two places99THEN it is a DAG. Decide whether aggregation is per edge/path or per node identity;100 neither is universally correct. Forbid sharing or track visited identity when101 the chosen semantics require it.102103IF an operation over the tree needs to know each node's concrete type104THEN it is a Visitor or a pattern-matched fold, not a method on105 Component (gof-visitor).106107IF children are fetched lazily from a database108THEN inspect query counts and required subtree size. Batch, prefetch, page or query the needed109 aggregate; neither a query per node nor loading an unbounded whole tree is inevitable110 (orm-behavioral-patterns).111```112113## Cross-cutting checks114115- **Concurrency.** Nothing about the pattern is thread-safe. The realistic hazards are a116 traversal running while a child is added — `ConcurrentModificationException` in some117 collection implementations, a walk118 that silently skips a subtree at worst — and a "total" computed across a mutation, which is119 arithmetically consistent with no state the tree ever had. Immutable nodes with a copy-on-write120 root reference remove both only when the reachable nodes are deeply immutable, and then make121 caching a computed aggregate safe.122- **Distribution.** A composite is process-local. Where children are references into another123 service, the uniform interface turns one call into a fan-out whose latency is the slowest124 branch and whose failure semantics are partial (`scatter-gather`). Where trees are transmitted,125 depth is an attack surface — deeply nested JSON or XML exhausts the parser's stack or the126 serialiser's, so a depth limit belongs at the boundary, not in the domain.127- **Performance.** Per-node object overhead can dominate wide shallow trees of small payloads;128 measure layout rather than infer it from node count. Recursive traversal consumes native129 thread-stack space and HotSpot generally does not perform tail-call elimination. Where a tree is walked130 in a hot path, consider computing an aggregate incrementally at mutation time, or flattening to131 an array-backed representation — after measuring (`allocation-profiling`).132- **Testing.** Trees are where property-based tests pay: generate random structures and assert133 invariants (total of a branch equals the sum of its children; a walk visits every node once;134 depth-limit rejection). Include a degenerate deep chain in the suite — that is the case135 production finds and unit tests miss.136137## Review checklist138139- [ ] The recursion is real, not a two-level group modelled aspirationally140- [ ] No leaf implements an operation by throwing141- [ ] Depth from external input is bounded at the boundary142- [ ] Traversal is iterative where depth is unbounded, or depth is provably small143- [ ] `equals`, `hashCode` and `toString` terminate in the presence of parent pointers144- [ ] Mutation and traversal cannot overlap, or the nodes are immutable145- [ ] Node sharing is either forbidden or accounted for in every aggregation146- [ ] Children are not loaded lazily per node inside a walk147148## References149150- [Structure and hazards](references/structure-and-hazards.md) — transparent, safe and sealed151 compared with what each costs, iterative traversal, depth bounding, cycles and identity,152 `equals`/`hashCode` on recursive structures, parent pointers, and the mutation-versus-traversal153 rules. Read before implementing a tree that outlives a single method.154- [Worked example](references/worked-example.md) — an organisational permission tree: the155 transparent version and its throwing leaf, the sealed version, an iterative resolver with a156 depth bound, caching an aggregate safely under immutability, and the property tests. Read when157 implementing.