100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Immutable Data Structures

Explore Clojure's core persistent data structures — vectors, lists, maps, and sets — and understand how immutability and structural sharing make them both safe and efficient.

Core ConceptsBeginner9 min readJul 10, 2026
Analogies

Persistent, Immutable Collections

Every core Clojure collection — vectors, lists, maps, and sets — is immutable: once created, a collection's contents never change. Operations that appear to 'modify' a collection, such as conj, assoc, or dissoc, always return a brand-new collection and leave the original untouched. This is called a persistent data structure, because old versions of the collection remain valid and accessible even after new versions are created. For example, (def v1 [1 2 3]) followed by (def v2 (conj v1 4)) leaves v1 as [1 2 3] while v2 becomes [1 2 3 4].

🏏

Cricket analogy: conj creating a new vector instead of mutating v1 is like a scorer never erasing the over-by-over card from the 10th over of a Cricket World Cup final — each new ball's outcome is recorded as a fresh entry, so the original 10th-over record stays intact forever.

Structural Sharing

If every 'change' created a full copy, immutability would be prohibitively slow for large collections. Clojure avoids this with structural sharing: internally, vectors and maps are implemented as tries (branching tree structures), so conj or assoc only needs to copy the small path of nodes from the root down to the changed leaf, while every unchanged branch is shared between the old and new versions. This gives near-constant-time performance for common operations even though the collection is logically immutable.

🏏

Cricket analogy: Structural sharing is like a team only updating the specific batting-order slot that changed on the electronic scoreboard rather than reprinting the entire scorecard from scratch every time a wicket falls in an IPL match.

Vectors and Lists: Choosing the Right Sequential Structure

Clojure has two primary sequential collections. Lists, written (1 2 3) or built with list, are linked structures optimized for adding and removing elements at the front with conj and cons — they are also what Clojure code itself is made of, since a function call like (+ 1 2) is literally a list. Vectors, written [1 2 3], are indexed structures optimized for adding elements at the end with conj and for fast random access with get or nth(nth [10 20 30] 1) returns 20 in effectively constant time, whereas indexing into a list requires walking from the front.

🏏

Cricket analogy: Choosing a vector for indexed access is like a scorer instantly looking up 'who faced ball 47 of the innings' from a numbered ball-by-ball array, whereas a list is more like a stack of handwritten notes where finding ball 47 means flipping through from ball 1.

Maps and Sets

Clojure's hash-map associates keys with values and provides average constant-time lookup: (get {:name "Ada" :lang "Clojure"} :lang) returns "Clojure". sorted-map maintains keys in sorted order at the cost of logarithmic-time operations. Sets, created with hash-set or the #{} literal, store unique unordered elements and support contains? for constant-time membership tests: (contains? #{:a :b :c} :b) returns true. Because maps and sets are themselves immutable, they can safely be used as keys or values inside other maps and sets without fear of the nested structure changing underneath you.

🏏

Cricket analogy: A hash-map is like an ICC player database where you look up Virat Kohli's average by name directly, while a sorted-map is like the official batting-average leaderboard that always stays ranked from highest to lowest as new innings are added.

clojure
(def team {:name "Alice" :role "Engineer" :skills #{:clojure :datomic}})

;; "Modifying" returns a new map; the original is untouched
(def team2 (assoc team :role "Senior Engineer"))
(:role team)   ;=> "Engineer"
(:role team2)  ;=> "Senior Engineer"

;; Vectors: fast indexed access
(def scores [88 95 76 91])
(nth scores 2)          ;=> 76
(conj scores 100)       ;=> [88 95 76 91 100]
scores                  ;=> [88 95 76 91]  (unchanged)

;; Sets: unique, unordered membership
(def tags #{:beginner :clojure :functional})
(contains? tags :clojure) ;=> true
(conj tags :clojure)      ;=> #{:beginner :clojure :functional} (no duplicate added)

Because Clojure's core collections are immutable, they are automatically safe to share across threads without locks — there is no risk of one thread seeing a partially-updated vector or map while another thread is writing to it. This is a foundational reason Clojure is well suited to concurrent programming.

Immutable updates like assoc and conj always return a new collection — they do not modify the original in place. A common beginner mistake is calling (conj my-vector 4) and expecting my-vector itself to change; you must capture the return value, e.g. (def my-vector (conj my-vector 4)).

  • All of Clojure's core collections — lists, vectors, maps, sets — are immutable and persistent: 'updates' return new versions rather than mutating in place.
  • Structural sharing (via trie-based internal structures) lets persistent collections perform near-constant-time updates without copying the entire structure.
  • Lists are optimized for adding/removing at the front and are the structure Clojure code itself is made of.
  • Vectors are optimized for adding at the end and for fast indexed access via nth or get.
  • Hash-maps give average constant-time key lookup; sorted-maps maintain key order at logarithmic cost.
  • Sets store unique, unordered elements and support constant-time membership checks with contains?.
  • Immutability makes Clojure collections inherently safe to share across threads without locks.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ClojureStudyNotes#ImmutableDataStructures#Immutable#Data#Structures#Persistent#StudyNotes#SkillVeris#ExamPrep