Skip to content
AgileLogics
5 min read

Records and Types

Prerequisites: Protocols

Most Clojure data is plain maps. When you need a named type — for protocol dispatch, Java interop, or performance — use a record.

(defrecord Point [x y])

(def p (->Point 1 2))                  ; positional constructor
(def q (map->Point {:x 3 :y 4}))       ; map constructor

(println p)
(println (:x p) (:y q))
(println (assoc p :x 10))              ; still a Point
(println (assoc p :label "origin-ish")) ; extra keys are allowed
(println (= p (->Point 1 2)))
(println (map? p) (instance? Point p))
(println (dissoc p :x))                ; removing a field: plain map
#user.Point{:x 1, :y 2}
1 4
#user.Point{:x 10, :y 2}
#user.Point{:x 1, :y 2, :label origin-ish}
true
true true
{:y 2}
  • defrecord generates a Java class with the fields, plus ->Point and map->Point constructor functions.
  • A record is a map: keyword access, assoc, update, destructuring, and every map function work.
  • Records have value equality, and field access is faster than map lookup.
  • Removing a declared field turns it back into a plain map.

deftype

(deftype Counter [^:volatile-mutable n]
  clojure.lang.IDeref
  (deref [_] n)
  Object
  (toString [_] (str "Counter(" n ")")))

(def c (Counter. 5))
(println @c (str c))
(println (map? c))
5 Counter(5)
false

deftype creates a bare class with no map behavior — for low-level building blocks like custom collections or mutable counters. Application code rarely needs it.

Choosing

Use For
Plain map Almost all domain data
defrecord Protocol implementations; hot paths; Java interop
deftype New data structures or low-level types
reify A one-off object implementing an interface or protocol

Start with maps; switch to records when you have a concrete reason. Code written against maps keeps working, because records are maps.