Advanced Clojure Tutorial
Table of Contents
- Prerequisites
- Project Setup
- Morning Session: Protocols, Records, and Reify
- Afternoon Session 1: Core.Async
- Afternoon Session 2: Transducers
- Evening Session: Advanced Topics
- Exercises and Projects
- Best Practices and Tips
- Additional Resources
- Setup Instructions
- Notes
- 2026 Review: State of the Clojure Ecosystem
Prerequisites
- Solid understanding of Clojure fundamentals (functions, collections, immutability)
- Experience with basic Clojure development
- Familiarity with Java interop concepts
Project Setup
(defproject advanced-clojure-tutorial "0.1.0-SNAPSHOT" :description "Advanced Clojure Tutorial Project" :dependencies [[org.clojure/clojure "1.11.1"] [org.clojure/core.async "1.6.673"] [com.stuartsierra/component "1.1.0"] [org.clojure/spec.alpha "0.3.218"]] :main ^:skip-aot tutorial.core :target-path "target/%s" :profiles {:uberjar {:aot :all :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})
Morning Session: Protocols, Records, and Reify
Basic Protocol and Record Example
(ns tutorial.shapes) (defprotocol Drawable (draw [this] "Draws the shape") (area [this] "Calculates the area")) (defrecord Circle [radius] Drawable (draw [this] (str "Drawing circle with radius " radius)) (area [this] (* Math/PI radius radius))) ;; Example usage in comment block for CIDER evaluation (comment (def my-circle (->Circle 5)) (draw my-circle) (area my-circle) )
Reify Deep Dive
(ns tutorial.reify-examples (:require [tutorial.shapes :refer [Drawable]])) ;; Creating anonymous implementations (def square (reify Drawable (draw [this] "Drawing a square") (area [this] 100))) ;; Java interface implementation (def runnable (reify java.lang.Runnable (run [this] (println "Running!")))) ;; Multiple protocol implementation (defprotocol Scalable (scale [this factor])) (def scalable-circle (reify Drawable (draw [this] "Drawing circle") (area [this] 50) Scalable (scale [this factor] (str "Scaling by " factor)))) (comment ;; Test implementations (draw square) (area square) (.run runnable) (draw scalable-circle) (scale scalable-circle 2) )
Afternoon Session 1: Core.Async
(ns tutorial.async-examples (:require [clojure.core.async :refer [chan go >! <! <!! timeout close! dropping-buffer sliding-buffer]])) ;; Basic channel operations (def c (chan)) (defn basic-channel-demo [] (go (>! c "Hello")) (go (println (<! c)))) ;; Buffer types demonstration (defn buffer-examples [] (let [buffered-chan (chan 10) ;; Fixed buffer dropping-chan (chan (dropping-buffer 10)) sliding-chan (chan (sliding-buffer 10))] ;; Example usage in real code would go here )) ;; Advanced patterns (defn async-processor [] (let [in (chan) out (chan)] (go-loop [] (when-let [val (<! in)] (>! out (str val " processed")) (recur))) [in out])) ;; Pipeline example (defn process-data [] (let [[in out] (async-processor)] (go (doseq [x (range 5)] (>! in x) (<! (timeout 100))) (close! in)) (go-loop [] (when-let [result (<! out)] (println result) (recur))))) (comment (basic-channel-demo) (process-data) )
Afternoon Session 2: Transducers
(ns tutorial.transducer-examples (:require [clojure.core.async :refer [chan]])) ;; Basic transducer composition (def xform (comp (map inc) (filter even?) (take 5))) ;; Custom transducer (defn dedupe-neighbors [step] (let [prev (volatile! ::none)] (fn ([] (step)) ([result] (step result)) ([result input] (let [prior @prev] (vreset! prev input) (if (= prior input) result (step result input))))))) ;; Advanced transducer patterns (def process-events (comp (map #(assoc % :timestamp (System/currentTimeMillis))) (filter :valid?) (dedupe-neighbors) (partition-all 100) (map #(assoc % :batch-id (random-uuid))))) ;; Example with core.async (def processing-channel (chan 1 process-events)) (comment ;; Test basic transducer (sequence xform (range 10)) ;; Test custom deduping (sequence (dedupe-neighbors) [1 1 2 2 2 3 3 1]) ;; Test process-events (let [events [{:valid? true :data "test1"} {:valid? true :data "test2"}]] (sequence process-events events)) )
Evening Session: Advanced Topics
Component Lifecycle Management
(ns tutorial.component-examples (:require [com.stuartsierra.component :as component])) (defrecord Database [uri connection] component/Lifecycle (start [this] (assoc this :connection (str "Connected to: " uri))) (stop [this] (println "Disconnecting from:" uri) (assoc this :connection nil))) (defn new-database [uri] (map->Database {:uri uri})) (defn create-system [] (component/system-map :db (new-database "postgresql://localhost:5432/mydb"))) (comment (def system (create-system)) (def started-system (component/start system)) (def stopped-system (component/stop started-system)) )
Spec Examples
(ns tutorial.spec-examples (:require [clojure.spec.alpha :as s] [clojure.spec.gen.alpha :as gen])) (s/def ::age (s/and int? #(>= % 0) #(<= % 150))) (s/def ::name string?) (s/def ::person (s/keys :req [::name ::age] :opt [::address])) (comment ;; Validation examples (s/valid? ::person {::name "John" ::age 30}) ;; Generation examples (gen/generate (s/gen ::person)) ;; Explain examples (s/explain ::person {::name "John" ::age -5}) )
Exercises and Projects
Exercise 1: Web Scraper
(ns tutorial.exercises.web-scraper (:require [clojure.core.async :refer [chan go >! <! <!! timeout close!]])) ;; TODO: Implement a concurrent web scraper ;; 1. Create channels for URLs and results ;; 2. Implement worker processes using go blocks ;; 3. Add error handling ;; 4. Implement rate limiting
Exercise 2: Data Pipeline
(ns tutorial.exercises.data-pipeline) ;; TODO: Implement a data processing pipeline using transducers ;; 1. Define data transformation steps ;; 2. Compose transducers ;; 3. Add error handling and validation ;; 4. Implement batching and throttling
Best Practices and Tips
- Use reify for one-off implementations, defrecord for reusable types
- Always close channels in core.async to prevent memory leaks
- Consider using transducers early in development for performance benefits
- Use component or mount for managing application state
- Leverage spec for validation and documentation
Additional Resources
- Clojure core.async documentation: https://clojure.github.io/core.async/
- "Clojure Applied" by Ben Vandgrift and Alex Miller
- Stuart Sierra's Component library: https://github.com/stuartsierra/component
- Transducers video series by Rich Hickey: https://www.youtube.com/watch?v=6mTbuzafcII
Setup Instructions
- Clone this repository
- Open in your favorite editor with CIDER support
- Run
C-c C-v t(cider-eval-buffer) to tangle the source files - Connect to a REPL and start experimenting!
Notes
- All code blocks are tangled to appropriate files in the src directory
- Comment blocks contain examples that can be evaluated directly in CIDER
- Use
C-c C-v tto tangle all source files at once - Use
C-c C-cto evaluate individual code blocks - The project structure follows standard Clojure conventions
2026 Review: State of the Clojure Ecosystem
Two years on from the original tutorial, the Clojure ecosystem has matured significantly. This section surveys the major releases, tooling shifts, and emerging use cases through early 2026.
Clojure 1.12 (September 2024)
Clojure 1.12 shipped with focused Java interop improvements that reduce the friction of working at the JVM boundary.
Key additions:
- Method values —
ClassName/methodsyntax lifts Java methods into first-class Clojure functions without a wrapping anonymous fn. - add-lib promoted to core —
clojure.repl.deps/add-libandsync-depsallow adding dependencies to a running REPL without restart, previously an unofficial Clojure CLI extension. - Array-class syntax —
String/1,long/2etc. for typed array construction/coercion. - Qualified method hints —
^[StringBuilder append]hint syntax replaces reflection on overloaded methods.
;; Clojure 1.12 — method values and add-lib (ns examples.clj12) ;; Method value: String/toUpperCase as a plain function (def upper String/toUpperCase) (map upper ["hello" "clojure" "1.12"]) ;; => ("HELLO" "CLOJURE" "1.12") ;; Add a dependency to a running REPL (no restart) ;; (require '[clojure.repl.deps :as deps]) ;; (deps/add-lib 'com.github.seancorfield/next.jdbc {:mvn/version "1.3.939"}) ;; Array coercion with new syntax (let [arr (long/1 5)] ; long[] of length 5 (aset arr 0 42) (aget arr 0)) ;; => 42
Clojure 1.13 Alpha (2026)
As of Q1 2026, Clojure 1.13 is in alpha. The stated design goals lean toward developer-ergonomics at the REPL boundary:
- Keyword argument improvements — functions accepting keyword args can now receive a
trailing map, unifying
applypatterns that previously needed manual splicing. - Error message initiative — better spec-backed messages for common mistakes (wrong arity, missing key, etc.) without requiring a separate dependency.
- Spec 2 integration path — the alpha includes provisional hooks for spec 2 conformance, though spec 2 remains a separate library.
- Performance work on small-map persistent data structures (similar to Kotlin value classes) is under discussion but not landed.
Track progress at clojure.org/news and the ask.clojure.org forum.
ClojureScript in 2026
ClojureScript compilation targets have split into three distinct tools serving different audiences:
| Tool | Compilation model | Primary audience |
|---|---|---|
| shadow-cljs | Full CLJS, JS bundler | Production SPAs, Node targets |
| cherry | JS-first CLJS subset | Embeddable, lightweight apps |
| squint | Transpiler to ES | Scripting, edge runtimes |
shadow-cljs remains the production standard. The 2024–2026 releases improved
hot-reload reliability, added first-class support for Bun and Deno targets, and
streamlined the :node-library output mode used by shared npm packages.
cherry and squint (Thomas Heller / Michiel Borkent) target environments where loading the full CLJS compiler is too heavy. Cherry retains ClojureScript semantics; squint trades some semantics for zero-overhead ES module output. Both are useful for Cloudflare Workers and Deno Deploy where cold-start size matters.
;; squint — ClojureScript-like syntax compiling to plain ES modules ;; run with: npx squint compile squint_demo.cljs (defn greet [name] (str "Hello, " name "!")) (println (greet "squint"))
Babashka Maturity
Babashka (bb) has graduated from novelty to infrastructure:
- Scripting — Babashka is now the de facto standard for Clojure shell scripting.
Common patterns (HTTP calls, file ops, JSON) require no extra deps thanks to the
bundled
babashka.http-client,cheshire, andclojure.java.ioequivalents. - CI integration — GitHub Actions workflows using
bb tasksreplace multi-hundred-line shell scripts with testable Clojure. Thebabashka/setup-babashkaaction makes installation one line. - Pods — the pod protocol allows bb scripts to call out-of-process libraries written
in any language, side-stepping JVM startup while preserving access to JVM-only code.
Active pods include
pod-babashka-postgresql,pod-babashka-aws, andpod-babashka-fswatcher. - nREPL compatibility —
bb nrepl-serverprovides a full nREPL so CIDER and Calva work against bb scripts without modification.
;; bb tasks — invoke via: bb test, bb lint, bb format (ns bb.tasks (:require [babashka.process :refer [shell]])) (defn run-tests [] (shell "clojure -M:test")) (defn run-lint [] (shell "clj-kondo --lint src test")) (defn format-all [] (shell "cljfmt fix src test"))
Clojure in the Agent Era
The AI/agent wave has touched Clojure in a few practical ways:
- MCP servers in Clojure — the Model Context Protocol (Anthropic, 2024) is
transport-agnostic JSON-RPC. Several community projects expose MCP servers written in
Clojure/Babashka.
bbis well-suited to lightweight MCP tool handlers because startup time is <50 ms. - Bosquet / Malli-based prompt schemas — bosquet uses Selmer templating and Malli for typed prompt structures, a natural fit for Clojure's data-oriented approach.
- LLM clients —
wkok/openai-clojurewraps OpenAI and Azure OpenAI APIs; community wrappers exist for Anthropic's Messages API and Bedrock. - Agent loops in core.async — the channel/go-block model maps cleanly onto tool-call/result loops, making core.async a natural fit for multi-step agent orchestration without a framework.
- Datomic as agent memory — the immutable log model means full history is queryable at any point, which maps well onto agent state snapshotting and replay.
Datomic Going Open Source (Nubank, 2023–2024)
In late 2023 Nubank open-sourced the Datomic transactor and peer library under the Apache 2.0 license. Previously Datomic Pro required a commercial license; Datomic Cloud remained AWS-hosted. The open-source release includes:
- The full peer library (
com.datomic/peer) - The transactor binary
- The console UI
Practical impact: teams can now self-host Datomic without license cost, store the transactor config in their own infrastructure-as-code, and ship Datomic in Docker or FreeBSD jails without a Cognitect account. Datomic Cloud remains a separate offering.
See datomic-is-free and Datomic Free on GitHub.
Ecosystem Map
The diagram below shows how the major Clojure runtimes relate to each other and to their host environments.
// Clojure ecosystem 2026 — runtime families and their relationships digraph clojure_ecosystem { rankdir=LR; graph [bgcolor="white", fontname="Helvetica", fontsize=11, pad="0.4", nodesep="0.35", ranksep="0.6"]; node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=10, fillcolor="#f5f5f5", color="#888"]; edge [color="#aaa"]; // Host runtimes jvm [label="JVM (HotSpot / OpenJ9)", fillcolor="#ddeeff", color="#336"]; node_js [label="Node / Deno / Bun", fillcolor="#ddeeff", color="#336"]; native [label="Native Binary", fillcolor="#ddeeff", color="#336"]; browser [label="Browser (ES Module)", fillcolor="#ddeeff", color="#336"]; // Clojure runtimes clojure [label="Clojure 1.12 / 1.13", fillcolor="#e8f5e9", color="#2e7d32"]; cljs [label="ClojureScript\n(shadow-cljs)", fillcolor="#e8f5e9", color="#2e7d32"]; cherry [label="cherry", fillcolor="#fff9c4", color="#f57f17"]; squint [label="squint", fillcolor="#fff9c4", color="#f57f17"]; bb [label="Babashka", fillcolor="#fce4ec", color="#880e4f"]; graal [label="GraalVM native-image", fillcolor="#f3e5f5", color="#4a148c"]; // Libraries / tooling datomic [label="Datomic (OSS, 2023)", fillcolor="#e0f2f1", color="#00695c"]; malli [label="Malli / spec 2", fillcolor="#e0f2f1", color="#00695c"]; mcp [label="MCP tools\n(bb / Clojure)", fillcolor="#e0f2f1", color="#00695c"]; // Runtime relationships clojure -> jvm [label="runs on"]; cljs -> node_js [label="targets"]; cljs -> browser [label="targets"]; cherry -> browser [label="ES module"]; squint -> browser [label="ES module"]; squint -> node_js [label="ES module"]; bb -> graal [label="compiled via"]; graal -> native [label="produces"]; clojure -> graal [label="AOT via"]; // Library relationships clojure -> datomic [label="peer lib"]; clojure -> malli [label="uses"]; bb -> mcp [label="hosts"]; clojure -> mcp [label="hosts"]; }
