Vectors, Lists, Maps, and Sets in Clojure

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.

Comparison of Clojure vectors, lists, maps, and sets

Why Collection Choice Matters

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:

  • use vectors for ordered application data
  • use maps for structured domain data
  • use sets for uniqueness and membership
  • use lists mainly when code-as-data or front-prepend semantics are the point

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.

Vectors

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:

  • position matters
  • you want stable ordering
  • you need nth, peek, assoc, or subvec
  • the data should read like an ordered list of values rather than executable code

Vectors are especially good for UI state, ordered options, parsed tokens, event batches, and most “list of things” cases in business code.

Lists

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:

  • you are treating code as data
  • front-prepend behavior is exactly what you want
  • you are working with macro forms or reader-produced lists

Avoid lists as the default container for ordinary domain data unless their semantics genuinely fit the problem.

Maps

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:

  • the data has named fields
  • lookup by meaning matters more than position
  • you expect the shape to evolve over time
  • destructuring and keyword access should stay readable

Most request payloads, configuration values, entities, and nested application state in Clojure end up as maps for good reason.

Sets

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:

  • uniqueness is part of the model
  • membership checks are frequent
  • duplicate values would be misleading
  • set algebra such as union or intersection matches the problem

If duplicates matter, a set is the wrong shape. If order matters, a set is also the wrong shape.

A Useful Default Heuristic

When you are not sure which collection to choose, ask what question the caller will ask most often:

  • “What is the item at position n?” suggests a vector.
  • “What fields belong to this thing?” suggests a map.
  • “Is this value present?” suggests a set.
  • “Is this form meant to be read and rewritten like code?” suggests a list.

That heuristic is more reliable than memorizing a table of asymptotic costs without context.

Common Mistakes

Treating Lists as the Default Sequence Type

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.

Using Maps When the Real Model Is a Set

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.

Using Vectors for Record-Like Data

["Ada" 42 true] is much harder to understand than {:name "Ada" :id 42 :active? true}. Named fields usually beat positional guessing.

Expecting Set Order

Sets are about presence, not ordering. If your logic depends on display order or insertion order, choose a different collection.

Design Review Question

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.

Key Takeaways

  • vectors are the normal default for ordered values
  • lists are specialized and most useful when code shape or front-prepend behavior matters
  • maps model named attributes
  • sets model uniqueness and membership
  • the best collection is usually the one that matches the question the code needs to ask
Loading quiz…
Revised on Thursday, April 23, 2026