Use Clojure maps as immutable records and lookup tables with keyword keys, assoc, update, dissoc, merge, nested updates, and Java HashMap comparisons.
Maps are central to everyday Clojure. They often replace a mix of Java DTOs, HashMaps, small configuration classes, and ad hoc records.
The key shift is that Clojure maps are values. You do not mutate them in place; you derive new maps.
1(def user
2 {:user/id 42
3 :user/email "dev@example.com"
4 :user/status :active})
Keywords are the usual internal keys because they are compact, self-evaluating, and readable. Qualified keywords such as :user/id help larger systems avoid collisions.
You have several idiomatic lookup options:
1(get user :user/email)
2;; => "dev@example.com"
3
4(:user/email user)
5;; => "dev@example.com"
6
7(user :user/email)
8;; => "dev@example.com"
Keyword-as-function is common for simple field access. get is clearer when you want a default:
1(get user :user/role :guest)
2;; => :guest
| Operation | Example | Result |
|---|---|---|
| Add or replace | (assoc user :user/name "Ada") |
New map with name |
| Remove | (dissoc user :user/status) |
New map without status |
| Transform existing value | (update user :user/email str/lower-case) |
New map with lowercased email |
| Nested replace | (assoc-in order [:customer :email] email) |
New nested map |
| Nested transform | (update-in order [:totals :cents] + 100) |
New nested map |
Example with a namespace require:
1(require '[clojure.string :as str])
2
3(update user :user/email str/lower-case)
4;; => {:user/id 42
5;; :user/email "dev@example.com"
6;; :user/status :active}
The original user map is unchanged.
Java HashMap habit |
Clojure map form | Difference |
|---|---|---|
put(k, v) |
(assoc m k v) |
Returns a new map |
remove(k) |
(dissoc m k) |
Returns a new map |
get(k) |
(get m k) or (k m) when k is a keyword |
No casting ceremony in typical Clojure code |
putAll(other) |
(merge m other) |
Later maps win |
| Nested mutable objects | assoc-in, update-in |
Updates nested values immutably |
In Java, the object identity often stays the same while contents change. In Clojure, the value changes and the old value remains available.
merge combines maps. Later values win:
1(merge {:user/name "Ada"
2 :user/status :draft}
3 {:user/status :active
4 :user/email "ada@example.com"})
5;; => {:user/name "Ada"
6;; :user/status :active
7;; :user/email "ada@example.com"}
Use merge-with when conflicts should be combined:
1(merge-with + {:errors 2 :warnings 1}
2 {:errors 3 :info 5})
3;; => {:errors 5 :warnings 1 :info 5}
Maps are sequences of entries when traversed:
1(seq {:a 1 :b 2})
2;; => ([:a 1] [:b 2])
To filter and keep a map result, use into {}:
1(into {}
2 (filter (fn [[_ v]] (pos? v)))
3 {:a 1 :b -2 :c 3})
4;; => {:a 1 :c 3}
Do not forget into {}. Plain filter returns a sequence of map entries, not a map.
Clojure maps work well for open domain data:
1(defn activate-user [user]
2 (assoc user :user/status :active))
3
4(defn active-user? [user]
5 (= :active (:user/status user)))
You can add schema, spec, Malli, or other validation later when the boundary needs stronger guarantees. The core representation can still be plain data.
id, email, and status into a qualified-key Clojure map.assoc, update, and dissoc without mutating the original map.merge-with to combine two count maps.assoc, dissoc, update, and merge return new maps.into {} when you need a map result.HashMap patterns.