Learn how Clojure's four core collection families differ in lookup model, update cost, and semantic meaning so you can pick the right default shape.
Choosing a Clojure collection is really a choice about intent. Are you modeling an ordered bag of values, a stack-like sequence, an entity with named fields, or a membership set? If that choice is vague, the code usually becomes vague too.
The visual below compares the four collection families by delimiter, access style, and the kind of question each one answers most naturally.
All four core Clojure collections are immutable and participate in the sequence abstraction, but they are not interchangeable. Each one makes some operations feel natural and others feel awkward. That is why experienced Clojure code tends to settle on a few reliable defaults:
That last point matters. Beginners often assume lists are the “normal” sequential collection because Lisp syntax uses parentheses. In ordinary application data, vectors are usually the better default.
A vector is an indexed, ordered collection written with square brackets, such as [10 20 30]. In everyday Clojure programs, vectors are the default choice for ordered data because they balance readability with efficient indexed lookup and append-like growth through conj.
1(def queue-like-items [:todo :doing :done])
2
3(nth queue-like-items 1)
4;; => :doing
5
6(conj queue-like-items :archived)
7;; => [:todo :doing :done :archived]
8
9(assoc queue-like-items 0 :backlog)
10;; => [:backlog :doing :done]
Use vectors when:
nth, peek, assoc, or subvecVectors are especially good for UI state, ordered options, parsed tokens, event batches, and most “list of things” cases in business code.
A list is a sequential collection written with parentheses when quoted, such as '(inc x). Lists are linked and optimized around front operations, but their most important role in Clojure is often conceptual: they are the natural shape for code forms and macros.
1(def quoted-form '(+ 1 2 3))
2
3(first quoted-form)
4;; => +
5
6(rest quoted-form)
7;; => (1 2 3)
8
9(conj quoted-form :start)
10;; => (:start + 1 2 3)
That last example shows why lists are easy to misuse. conj adds at the front of a list, not the end. If you want an ordered collection that grows at the end and reads like data, a vector is usually clearer.
Use lists when:
Avoid lists as the default container for ordinary domain data unless their semantics genuinely fit the problem.
A map is a key-value collection, usually written with curly braces, such as {:id 42 :name "Ada"}. Maps are the center of most Clojure domain modeling because they express named attributes directly and compose well with keyword lookup.
1(def user {:id 42
2 :name "Ada"
3 :roles #{:admin :editor}})
4
5(:name user)
6;; => "Ada"
7
8(assoc user :active? true)
9;; => {:id 42, :name "Ada", :roles #{:admin :editor}, :active? true}
10
11(update user :roles conj :auditor)
12;; => {:id 42, :name "Ada", :roles #{:admin :editor :auditor}}
Use maps when:
Most request payloads, configuration values, entities, and nested application state in Clojure end up as maps for good reason.
A set is an unordered collection of unique elements, written with #{...}. Sets answer membership questions well: “Is this permission present?”, “Has this feature flag been enabled?”, “Which tags are associated with this object?”
1(def permissions #{:read :write})
2
3(contains? permissions :write)
4;; => true
5
6(conj permissions :admin)
7;; => #{:read :write :admin}
8
9(disj permissions :read)
10;; => #{:write}
Use sets when:
If duplicates matter, a set is the wrong shape. If order matters, a set is also the wrong shape.
When you are not sure which collection to choose, ask what question the caller will ask most often:
n?” suggests a vector.That heuristic is more reliable than memorizing a table of asymptotic costs without context.
In Clojure, parentheses show up everywhere, so new readers sometimes assume lists should hold most ordered data. They usually should not. Vectors are more natural for most application-facing collections.
A map like {:read true :write true} often wants to be a set such as #{:read :write}. If the values are just booleans repeating the same meaning, the map may be accidental complexity.
["Ada" 42 true] is much harder to understand than {:name "Ada" :id 42 :active? true}. Named fields usually beat positional guessing.
Sets are about presence, not ordering. If your logic depends on display order or insertion order, choose a different collection.
You receive a request payload describing a deployment:
1{:service "billing"
2 :regions ["ca-central-1" "us-east-1"]
3 :enabled-flags [:audit-log :async-retry :audit-log]
4 :owners ["platform" "payments"]}
Which parts of that model are reasonable, and which parts should likely change?
A strong answer notices that :regions and :owners may reasonably stay vectors because order may matter for presentation or priority, while :enabled-flags probably wants a set because duplicates add no meaning and membership checks are the likely operation.