Browse Clojure Foundations for Java Developers

Immutable Data Structures

Learn how persistent collections model change by returning new values with structural sharing.

In Clojure, the default collections are immutable:

  • maps
  • vectors
  • sets
  • lists

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.

Persistent Does Not Mean “Saved To Disk”

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.”

Updating A Value Means Returning A New Value

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.

Nested Updates Stay Manageable

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.

Structural Sharing Is The Practical Trick

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:

  • earlier versions remain available
  • updates are efficient enough for ordinary application code

You still need to care about performance in hot paths, but the default model is far more practical than “immutable means full copy.”

Choose The Collection By Access Pattern

The core collection types are all immutable, but they are optimized for different use cases:

  • maps for keyed lookup
  • vectors for indexed access and appending
  • sets for membership and uniqueness
  • lists for stack-like prepending and code forms

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.

Why This Feels Better Than Mutable Collections

For Java engineers, the payoff usually shows up in four places:

  • shared values are safe to read from multiple threads
  • change is explicit in the function body
  • tests can compare before/after values directly
  • you need fewer defensive copies and fewer “who owns this object?” discussions

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.

A Useful Migration Habit

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.

Knowledge Check: Persistent Collections

### What does `assoc` do to a map? - [x] Returns a new map with the key updated; the original map value is unchanged. - [ ] Mutates the original map in place. - [ ] Updates all matching keys in every map in the program. - [ ] Only works when the key is a string. > **Explanation:** Clojure maps are immutable values. `assoc` returns a new map, usually reusing most of the old structure internally. ### What does `(conj '(1 2) 3)` return? - [x] `(3 1 2)` - [ ] `(1 2 3)` - [ ] `[1 2 3]` - [ ] It throws because lists are immutable. > **Explanation:** Lists are optimized for adding at the front, so `conj` prepends when the target collection is a list. ### What is “structural sharing”? - [x] A new collection value reuses most of the old collection’s internal structure to avoid copying everything. - [ ] A global cache shared by all collections in the JVM. - [ ] A way to share mutable objects safely between threads. - [ ] A feature that only applies to lazy sequences. > **Explanation:** Structural sharing is what makes immutable updates practical. The new value keeps most of the old structure instead of cloning the entire collection.
Revised on Friday, April 24, 2026