Understand Clojure's persistent maps, vectors, sets, and lists as practical immutable values that support efficient updates through structural sharing.
Clojure’s default collections are immutable: maps, vectors, sets, and lists do not change in place. Instead, update operations return new values.
For Java engineers, the important point is that immutable does not mean “copy the whole object graph every time.” Clojure collections are persistent data structures.
Persistent collection: an immutable collection that can produce an updated version efficiently by reusing most of the existing structure.
This term causes early confusion:
| Word | Meaning here |
|---|---|
| Persistent collection | Old value remains available after an update |
| Persistent storage | Data survives process restart |
Clojure’s persistent collections are about value versions and structural sharing, not automatic database persistence.
1(require '[clojure.string :as str])
2
3(def user
4 {:user/email " A@EXAMPLE.COM "
5 :user/roles ["admin" "admin"]
6 :user/active? true})
7
8(def normalized-user
9 (-> user
10 (update :user/email #(str/lower-case (str/trim %)))
11 (update :user/roles set)
12 (assoc :user/source :import)))
13
14user
15;; => {:user/email " A@EXAMPLE.COM ", :user/roles ["admin" "admin"], :user/active? true}
16
17normalized-user
18;; => {:user/email "a@example.com", :user/roles #{"admin"}, :user/active? true, :user/source :import}
The original user remains unchanged. That is the habit to build: describe the next value instead of mutating the current one.
1(def order
2 {:order/id 42
3 :order/shipping {:address/city "Toronto"
4 :address/postal-code "M5V"}
5 :order/items [{:item/sku "A" :item/qty 1}
6 {:item/sku "B" :item/qty 2}]})
7
8(-> order
9 (assoc-in [:order/shipping :address/city] "Ottawa")
10 (update-in [:order/items 0 :item/qty] inc))
In Java, nested immutable updates often require builders, copy constructors, records, or manual defensive copying. In Clojure, nested value updates are ordinary operations.
| Collection | Best fit | Java comparison |
|---|---|---|
| Vector | Ordered indexed values | Immutable cousin of ArrayList |
| Map | Domain records and lookup tables | Immutable cousin of HashMap |
| Set | Membership and uniqueness | Immutable cousin of HashSet |
| List | Code forms and head-first sequences | Persistent linked-list shape |
conj follows the natural shape of the target collection:
1(conj [1 2] 3)
2;; => [1 2 3]
3
4(conj '(1 2) 3)
5;; => (3 1 2)
That is not arbitrary. conj adds where the collection can add efficiently.
Immutable data helps because:
You still model change over time. You just put changing identity behind explicit tools such as atoms, refs, agents, database transactions, or queues instead of making every collection mutable by default.