Learn how persistent collections model change by returning new values with structural sharing.
In Clojure, the default collections are immutable:
For a Java engineer, that can sound expensive or awkward at first. The important detail is that Clojure collections are usually persistent, not “copy the whole thing every time.”
Persistent collection: an immutable collection that can produce an updated version efficiently by reusing most of the existing structure.
That is what makes immutable programming practical on the JVM. You still represent change. You just represent it as a new value rather than as in-place mutation.
This is an easy early misunderstanding.
In Clojure, “persistent” means the old version of the collection is still available after you create a new one. It does not mean the collection is automatically durable or stored in a database.
Think of it as “version-preserving,” not “database-persistent.”
Instead of mutating an object, you derive a new value from the old one:
1(require '[clojure.string :as str])
2
3(def user {:email "A@EXAMPLE.COM"
4 :roles ["admin" "admin"]
5 :active? true})
6
7(def normalized-user
8 (-> user
9 (update :email str/lower-case)
10 (update :roles set)
11 (assoc :source :import)))
12
13user
14;; => {:email "A@EXAMPLE.COM", :roles ["admin" "admin"], :active? true}
15
16normalized-user
17;; => {:email "a@example.com", :roles #{"admin"}, :active? true, :source :import}
The original user value is unchanged. That makes transformations easier to test and easier to reason about in concurrent code.
Java developers are used to defensive copying or mutable setter chains when updating nested structures. Clojure gives you direct tools for this:
1(def order {:id 42
2 :shipping {:address {:city "Toronto"
3 :postal-code "M5V"}}
4 :items [{:sku "A" :qty 1}
5 {:sku "B" :qty 2}]})
6
7(-> order
8 (assoc-in [:shipping :address :city] "Ottawa")
9 (update-in [:items 0 :qty] inc))
This is one reason data-oriented design feels natural in Clojure. Nested updates are a normal operation, not a special case requiring custom copy constructors.
When you update a persistent collection, Clojure does not clone the whole structure just to change one field. It reuses most of the original data internally.
That gives you two important benefits at once:
You still need to care about performance in hot paths, but the default model is far more practical than “immutable means full copy.”
The core collection types are all immutable, but they are optimized for different use cases:
That is why conj behaves differently depending on the collection:
1(conj [1 2] 3) ;; => [1 2 3]
2(conj '(1 2) 3) ;; => (3 1 2)
conj adds in the collection’s natural efficient place. That is not inconsistency. It is a clue that each collection type has its own performance shape.
For Java engineers, the payoff usually shows up in four places:
You still model change over time. You simply move that concept to a reference type such as an atom or ref instead of baking mutability into every collection.
When you are tempted to ask, “Which object should own this update?” try replacing the question with:
“What pure transformation should turn the old value into the new value?”
That shift is one of the most important steps in moving from Java’s object-centric style to Clojure’s data-first style.