Learn how nested maps and vectors let Clojure model trees cleanly, apply uniform operations to leaves and branches, and update hierarchical data without object-heavy scaffolding.
Composite pattern: A structural pattern that lets a system treat individual items and groups of items through one recursive model.
Composite fits Clojure unusually well because the language already works comfortably with nested data. Where object-oriented code often builds a formal class hierarchy of Component, Leaf, and Composite, Clojure can often represent the same idea with maps, vectors, and a few recursive operations.
Composite is valuable when the caller should not have to care whether it is working with:
Examples include:
The common requirement is that traversal and updates should work uniformly.
1(def page-tree
2 {:node/type :section
3 :title "Docs"
4 :children [{:node/type :page
5 :title "Introduction"}
6 {:node/type :section
7 :title "Reference"
8 :children [{:node/type :page
9 :title "API"}
10 {:node/type :page
11 :title "CLI"}]}]})
This shape is small, readable, and friendly to ordinary sequence operations. The composite structure is in the data, not hidden behind an elaborate object graph.
The same recursive operations can walk the root section, the nested section, and each leaf page because they all share one predictable nested shape.
1(defn titles-in-tree [node]
2 (cons (:title node)
3 (mapcat titles-in-tree (:children node))))
4
5(defn branch? [node]
6 (seq (:children node)))
The important point is not clever recursion syntax. It is that the same logic can handle both leaves and branches.
For some tasks, Clojure’s tree-seq can make this even cleaner:
1(defn all-nodes [root]
2 (tree-seq branch? :children root))
The tree becomes easier to maintain when every node follows a small contract:
Once every branch uses different child keys or radically different shapes, the recursive code becomes messy and the composite benefit fades.
Clojure’s immutable updates work well for composites, but they are easiest when the path semantics are clear. That often means:
:children key for branch nodesDo not reach for clojure.zip by default. It is powerful, but plain recursive functions are often enough for read-mostly trees or straightforward updates.
If every node family has unrelated structure, callers lose the benefit of uniform traversal.
Sometimes a flat relational model plus derived grouping is simpler than a stored tree.
Zippers and elaborate path logic are useful, but only when the update behavior is complex enough to justify them.
Use composite when a domain is naturally hierarchical and callers need to process leaves and branches with one recursive model. In Clojure, start with plain nested data, keep the node shape explicit, and add heavier traversal tools only when the simple recursive version stops being enough.