Tutorial: Learn Clojure from Scratch (2026)
Clojure is a modern Lisp that runs on the JVM (and CLR, and JavaScript). I have used it for data pipelines, web applications, and distributed systems. The language design is opinionated: immutability by default, concurrency via software transactional memory, and a powerful macro system inherited from Lisp.
Working with Clojure changed how I think about state and identity. The persistent data structures (sharing structure between versions) make immutability practical even in performance-sensitive code.
Functions and REPL
Clojure is a Lisp-1: functions and values share the same namespace. defn defines functions, def defines values. The REPL (Read-Eval-Print Loop) is the primary development interface — you develop by connecting to a running process and evaluating forms interactively. This interactive development workflow is incredibly productive.
(defn greet [name]
(str "Hello, " name "!"))
(defn factorial [n]
(reduce * (range 1 (inc n))))
;; REPL interaction
;; user=> (greet "Alice")
;; "Hello, Alice!"
;; user=> (factorial 5)
;; 120
(defn map' [f coll]
(when-let [s (seq coll)]
(cons (f (first s)) (map' f (rest s)))))
Immutability
Data structures in Clojure are immutable and persistent. Modifying returns a new version while sharing unchanged parts with the original. List uses (), vector uses [], map uses {}, set uses #{}. assoc adds/replaces keys, dissoc removes them, conj adds elements. These all return new data structures.
(def m {:name "Alice" :age 30 :city "NYC"})
(assoc m :age 31)
;; => {:name "Alice", :age 31, :city "NYC"}
;; m still {:name "Alice" :age 30 :city "NYC"}
(dissoc m :city)
;; => {:name "Alice", :age 30}
(def v [1 2 3])
(conj v 4) ;; => [1 2 3 4]
(def s #{1 2 3})
(conj s 4) ;; => #{1 2 3 4}
(disj s 1) ;; => #{2 3}
Concurrency
Clojure provides multiple concurrency primitives. Atoms coordinate synchronous, independent state changes with swap! and reset!. Refs coordinate synchronous, coordinated changes across multiple identities with STM transactions. Agents handle asynchronous updates. future runs code in a thread pool. core.async provides CSP channels.
(def counter (atom 0))
(swap! counter inc)
@counter ;; => 1
(def account-a (ref 1000))
(def account-b (ref 500))
(dosync
(alter account-a - 200)
(alter account-b + 200))
(def agent-val (agent 0))
(send agent-val inc)
@agent-val
(def f (future (Thread/sleep 1000) 42))
@f ;; waits for result
Macros
Macros run at compile time and transform code. They receive unevaluated forms and produce new forms. The backtick (`) quotes with syntax-quote, ~ unquotes, ~@ splices. Macros enable DSLs and control structures. Unlike functions, macros control evaluation: they can delay, repeat, or prevent evaluation of arguments.
(defmacro unless [condition & body]
`(if (not ~condition)
(do ~@body)))
(unless false
(println "this runs")
(println "and this too"))
(defmacro infix [[a op b]]
`(~op ~a ~b))
(infix (1 + 2)) ;; => 3
;; Threading macro
(->> (range 10)
(filter odd?)
(map #(* % 2))
(reduce +))
Sequences
The sequence abstraction (seq) unifies all collections. map, filter, reduce, take, drop, partition work on any collection. Sequences are lazy by default — elements are computed on demand. This enables processing of infinite sequences and efficient pipeline composition without intermediate allocations.
(defn fibs []
(map first (iterate (fn [[a b]] [b (+ a b)]) [0 1])))
(take 10 (fibs))
;; => (0 1 1 2 3 5 8 13 21 34)
(defn process-lines [file]
(with-open [rdr (clojure.java.io/reader file)]
(->> (line-seq rdr)
(filter #(re-find #"ERROR" %))
(take 100)
(doall)))
(def numbers (range 1e6))
(reduce + (take 1000 (filter even? numbers)))
Java Interop
Running on the JVM gives Clojure access to Java libraries. Use . for method calls, new for constructors, doto for chained mutation. Import Java classes with :import. Clojure seamlessly interacts with Java collections, streams, and types. This makes millions of existing Java libraries available to Clojure code.
(import [java.util Date Calendar]
[java.net URL]
[java.time LocalDateTime])
;; Call methods
(.toUpperCase "hello")
;; Constructor
(def now (Date.))
;; Static method
(Calendar/DAY_OF_MONTH)
;; Chained calls
(doto (java.util.HashMap.)
(.put "key1" "val1")
(.put "key2" "val2"))
;; Java streams
(import '[java.util.stream Collectors])
(->> (range 10)
(map inc)
(.collect (Collectors/toList)))
Frequently Asked Questions
Clojure vs Common Lisp?
Clojure is a JVM Lisp with immutability by default, STM concurrency, and persistent data structures. Common Lisp has CLOS, multiple implementations, and a different community.
What is a persistent data structure?
A data structure that preserves previous versions when modified. Changes share structure with the original, making them memory-efficient. Clojure's are implemented with hash trie trees.
How does Clojure handle state?
State is separate from identity. Identities (atoms, refs, agents) coordinate threadsafe changes to immutable values. You manage transitions, not mutations.
What does the #() reader macro do?
Creates an anonymous function literal. #(+ %1 %2) is equivalent to (fn [a b] (+ a b)). % is %1, %2 is second arg, %& is rest args.
Originally published on Ayodhyyya. Last updated June 1, 2026.