Advanced Clojure Tutorial

Table of Contents

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

Setup Instructions

  1. Clone this repository
  2. Open in your favorite editor with CIDER support
  3. Run C-c C-v t (cider-eval-buffer) to tangle the source files
  4. 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 t to tangle all source files at once
  • Use C-c C-c to 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 valuesClassName/method syntax lifts Java methods into first-class Clojure functions without a wrapping anonymous fn.
  • add-lib promoted to core — clojure.repl.deps/add-lib and sync-deps allow adding dependencies to a running REPL without restart, previously an unofficial Clojure CLI extension.
  • Array-class syntaxString/1, long/2 etc. 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 apply patterns 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, and clojure.java.io equivalents.
  • CI integration — GitHub Actions workflows using bb tasks replace multi-hundred-line shell scripts with testable Clojure. The babashka/setup-babashka action 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, and pod-babashka-fswatcher.
  • nREPL compatibilitybb nrepl-server provides 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. bb is well-suited to lightweight MCP tool handlers because startup time is <50 ms.
  • Bosquet / Malli-based prompt schemasbosquet uses Selmer templating and Malli for typed prompt structures, a natural fit for Clojure's data-oriented approach.
  • LLM clientswkok/openai-clojure wraps 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"];
}

diagram-clojure-ecosystem.png

Author: Jason Walsh

j@wal.sh

Last Updated: 2026-04-19 08:30:00

build: 2026-04-20 23:41 | sha: d110973