← Back to all projects

LEARN CLOJURE DEEP DIVE

Learn Clojure: From Zero to Functional Programming Master

Goal: Deeply understand Clojure—from Lisp fundamentals and functional programming to macros, concurrency, ClojureScript, and building real-world systems that leverage immutability and the JVM ecosystem.


Why Learn Clojure?

Clojure is a modern, dynamic dialect of Lisp that runs on the Java Virtual Machine (JVM). Created by Rich Hickey in 2007, it represents a thoughtful fusion of ancient programming philosophy (Lisp, circa 1958) with modern engineering needs.

Learning Clojure will fundamentally change how you think about programming:

  • Immutability by default: Data structures don’t change—they produce new versions
  • Functional-first: Functions are first-class citizens; composition over mutation
  • Homoiconicity: Code is data, data is code—enabling powerful metaprogramming
  • Concurrency without pain: No locks, no race conditions, just simple semantics
  • REPL-driven development: Interactive, exploratory programming at its finest
  • JVM ecosystem access: Leverage billions of dollars of Java investment

After completing these projects, you will:

  • Think in functions, not objects
  • Write concurrent programs without fear
  • Create domain-specific languages with macros
  • Build full-stack applications with ClojureScript
  • Understand why parentheses are beautiful, not scary
  • See programming problems through a completely different lens

Core Concept Analysis

The Clojure Philosophy

┌────────────────────────────────────────────────────────────────────────────┐
│                          CLOJURE'S CORE VALUES                             │
├────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│   "We should aim for simplicity because simplicity is a prerequisite       │
│    for reliability." — Edsger W. Dijkstra                                  │
│                                                                            │
│   ┌──────────────┐    ┌──────────────┐    ┌──────────────┐                │
│   │  SIMPLICITY  │    │  GENERALITY  │    │   PRACTICALITY│                │
│   │              │    │              │    │              │                │
│   │ Simple ≠ Easy│    │ Few concepts │    │ Runs on JVM  │                │
│   │ Simple means │    │ that combine │    │ Interop with │                │
│   │ not complex  │    │ powerfully   │    │ Java libs    │                │
│   └──────────────┘    └──────────────┘    └──────────────┘                │
│                                                                            │
└────────────────────────────────────────────────────────────────────────────┘

Fundamental Concepts

1. Everything is an Expression

In Clojure, there are no statements—only expressions that return values:

;; A function call is just a list: (function arg1 arg2 ...)
(+ 1 2 3)       ; => 6
(if true "yes" "no")  ; => "yes"
(let [x 1] (+ x 2))   ; => 3

2. Immutable Data Structures

┌────────────────────────────────────────────────────────────────────────────┐
│                     PERSISTENT DATA STRUCTURES                             │
├────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│   List (linked list):    '(1 2 3 4)                                       │
│   Vector (indexed):      [1 2 3 4]                                         │
│   Map (key-value):       {:name "Alice" :age 30}                          │
│   Set (unique):          #{1 2 3 4}                                        │
│                                                                            │
│   These are IMMUTABLE:                                                     │
│   (conj [1 2 3] 4)  ; => [1 2 3 4]  ← returns NEW vector                  │
│   ; Original [1 2 3] is unchanged!                                        │
│                                                                            │
│   Implemented using STRUCTURAL SHARING:                                    │
│                                                                            │
│        v1 = [1 2 3]          v2 = (conj v1 4)                             │
│              │                       │                                     │
│              ▼                       ▼                                     │
│         ┌─────────┐            ┌─────────┐                                │
│         │ 1 2 3   │◄───────────│  (4)    │                                │
│         └─────────┘            └─────────┘                                │
│              │                       │                                     │
│         Shares structure!      Only adds new                              │
│                                                                            │
└────────────────────────────────────────────────────────────────────────────┘

3. Functions are First-Class

;; Functions as values
(def add-one (fn [x] (+ x 1)))
(def add-one #(+ % 1))  ; shorthand

;; Higher-order functions
(map inc [1 2 3])           ; => (2 3 4)
(filter even? [1 2 3 4])    ; => (2 4)
(reduce + [1 2 3 4])        ; => 10

;; Function composition
(def process (comp str inc))
(process 41)  ; => "42"

4. The Sequence Abstraction

┌────────────────────────────────────────────────────────────────────────────┐
│                         SEQUENCE ABSTRACTION                               │
├────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│   Everything that can be iterated is a "seq":                             │
│                                                                            │
│   (first [1 2 3])    ; => 1                                               │
│   (rest [1 2 3])     ; => (2 3)                                           │
│   (cons 0 [1 2 3])   ; => (0 1 2 3)                                       │
│                                                                            │
│   Same operations work on:                                                 │
│   - Lists, Vectors, Maps, Sets                                            │
│   - Strings (seq of characters)                                           │
│   - Java collections                                                       │
│   - Files (seq of lines)                                                  │
│   - XML/JSON (seq of nodes)                                               │
│                                                                            │
│   Lazy sequences:                                                          │
│   (take 5 (range))   ; => (0 1 2 3 4)                                     │
│   (range) is infinite, but we only compute what we need!                  │
│                                                                            │
└────────────────────────────────────────────────────────────────────────────┘

5. Concurrency Primitives

┌────────────────────────────────────────────────────────────────────────────┐
│                      CONCURRENCY IN CLOJURE                                │
├────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│   ┌────────────┐  ┌────────────┐  ┌────────────┐  ┌────────────┐          │
│   │   ATOMS    │  │    REFS    │  │   AGENTS   │  │ CORE.ASYNC │          │
│   ├────────────┤  ├────────────┤  ├────────────┤  ├────────────┤          │
│   │ Uncoord.   │  │ Coordinated│  │ Async      │  │ Channels   │          │
│   │ Synchronous│  │ Synchronous│  │ Independent│  │ Go blocks  │          │
│   │            │  │            │  │            │  │            │          │
│   │ (swap!     │  │ (dosync    │  │ (send      │  │ (go        │          │
│   │   atom     │  │   (alter   │  │   agent    │  │   (<! ch)) │          │
│   │   f)       │  │    ref f)) │  │   f)       │  │            │          │
│   └────────────┘  └────────────┘  └────────────┘  └────────────┘          │
│                                                                            │
│   Use case:                                                                │
│   - Atom: Single value, many readers/writers                              │
│   - Ref: Multiple values that must change together (STM)                  │
│   - Agent: Fire-and-forget async updates                                  │
│   - core.async: CSP-style channels (like Go)                              │
│                                                                            │
└────────────────────────────────────────────────────────────────────────────┘

6. Macros: Code as Data

;; Code is data (homoiconicity)
(quote (+ 1 2))  ; => (+ 1 2)  ← This is a LIST, not evaluated
'(+ 1 2)         ; => (+ 1 2)  ← Same thing, shorthand

;; Macros transform code at compile time
(defmacro unless [pred then else]
  `(if (not ~pred) ~then ~else))

(unless false "yes" "no")  ; => "yes"

;; The macro EXPANDS to:
(if (not false) "yes" "no")

How Clojure Differs from Other Languages

Aspect Clojure Java Python JavaScript
Paradigm Functional-first OOP Multi-paradigm Multi-paradigm
Mutability Immutable by default Mutable by default Mutable by default Mutable by default
Syntax S-expressions (Lisp) C-like Indentation C-like
Typing Dynamic, gradual (spec) Static Dynamic Dynamic
Concurrency STM, Atoms, Agents Locks, synchronized GIL (single-threaded) Event loop
Metaprogramming Macros (compile-time) Reflection, annotation Decorators, metaclasses Proxies
Platform JVM, JavaScript, CLR JVM CPython, PyPy Browser, Node
REPL First-class, interactive JShell (limited) Good Node REPL

Comparison with Other Lisps

Aspect Clojure Common Lisp Scheme Racket
Platform JVM/JS/CLR Native Various Native
Data Structures Immutable, persistent Mutable Mutable Both
Macros Hygienic-ish Unhygienic Hygienic Hygienic
Emphasis Concurrency, simplicity Power, flexibility Minimalism Teaching, PLT
Nil Handling nil is falsy nil is falsy #f is falsy #f is falsy
Modern Features Protocols, spec, transducers CLOS, conditions SRFI Contracts, types

Project List

The following 15 projects will take you from Clojure beginner to proficient functional programmer.


Project 1: REPL-Driven Calculator & Unit Converter

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: Clojure
  • Alternative Programming Languages: Common Lisp, Racket, Scheme
  • Coolness Level: Level 2: Practical but Forgettable
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 1: Beginner
  • Knowledge Area: REPL Development / Basic Syntax
  • Software or Tool: Leiningen, REPL
  • Main Book: “Clojure for the Brave and True” by Daniel Higginbotham

What you’ll build: An interactive calculator and unit converter that runs in the REPL, demonstrating basic Clojure syntax, function definitions, and the power of interactive development.

Why it teaches Clojure: The REPL is the heart of Clojure development. This project teaches you to think interactively—define functions, test them immediately, refine them live. You’ll internalize prefix notation and see why it’s actually cleaner than infix.

Core challenges you’ll face:

  • Understanding prefix notation → maps to why (+ 1 2) is better than 1 + 2
  • Defining pure functions → maps to no side effects, predictable outputs
  • Working with maps for conversions → maps to data-driven programming
  • REPL workflow → maps to interactive development as a superpower

Key Concepts:

  • Prefix Notation: “Clojure for the Brave and True” Ch. 3
  • Defining Functions: Clojure.org - Learn Clojure: Functions
  • Maps and Keywords: “Programming Clojure” Ch. 2 - Halloway & Bedra
  • REPL Workflow: “Living Clojure” Ch. 1 - Carin Meier

Difficulty: Beginner Time estimate: Weekend Prerequisites: Basic programming concepts (variables, functions, conditionals)

Real world outcome:

user=> (calc '(+ 2 (* 3 4)))
14

user=> (convert 100 :celsius :fahrenheit)
212.0

user=> (convert 5 :miles :kilometers)
8.0467

user=> (convert 1 :year :seconds)
31536000

user=> (history)
[{:op :convert :from :celsius :to :fahrenheit :value 100 :result 212.0}
 {:op :convert :from :miles :to :kilometers :value 5 :result 8.0467}]

Implementation Hints:

Start with the basics—defining simple functions:

;; Think about function structure:
;; (defn function-name [parameters] body)

;; For unit conversion, consider a data-driven approach:
;; Store conversion factors in a map
;; {:miles->kilometers 1.60934
;;  :celsius->fahrenheit (fn [c] (+ (* c 1.8) 32))}

;; For the calculator, think about:
;; - How do you evaluate a list like '(+ 1 2)?
;; - What if the arguments are themselves expressions?
;; - Hint: recursion is your friend

Questions to guide you:

  1. What’s the difference between def and defn?
  2. How do you create a map? How do you look up a value?
  3. What does the quote (‘) do and why do you need it for the calculator?
  4. How can you store history? (Hint: atoms for state)

Learning milestones:

  1. Basic arithmetic functions work → You understand prefix notation
  2. Unit converter uses data-driven lookups → You understand maps
  3. Calculator handles nested expressions → You understand recursion
  4. History is tracked → You understand atoms for state

Project 2: Personal Expense Tracker with Immutable Data

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: Clojure
  • Alternative Programming Languages: Haskell, Elm, F#
  • Coolness Level: Level 2: Practical but Forgettable
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 1: Beginner
  • Knowledge Area: Data Modeling / Immutable State
  • Software or Tool: Leiningen, edn files
  • Main Book: “Clojure for the Brave and True” by Daniel Higginbotham

What you’ll build: A command-line expense tracker that stores all data immutably, supports time-based queries, and generates spending reports.

Why it teaches Clojure: Real applications need to manage state. This project teaches you how Clojure handles state without mutation—you’ll add expenses by creating new collections, not modifying existing ones. You’ll see why this makes your code more predictable and testable.

Core challenges you’ll face:

  • Modeling data with maps and vectors → maps to thinking in data, not objects
  • Filtering and aggregating sequences → maps to higher-order functions
  • Persisting data with edn → maps to Clojure’s native data format
  • Managing application state with atoms → maps to safe state management

Key Concepts:

  • Maps and Vectors: “Clojure for the Brave and True” Ch. 4
  • Sequence Functions: Clojure.org - Sequences
  • edn Format: github.com/edn-format/edn
  • Atoms: “Programming Clojure” Ch. 6 - Halloway & Bedra

Difficulty: Beginner Time estimate: 1 week Prerequisites: Project 1 (REPL basics)

Real world outcome:

user=> (add-expense {:amount 42.50 :category :food :description "Lunch"})
{:id 1 :amount 42.50 :category :food :description "Lunch" :date #inst "2025-01-15"}

user=> (add-expense {:amount 15.00 :category :transport :description "Uber"})
{:id 2 :amount 15.00 :category :transport :description "Uber" :date #inst "2025-01-15"}

user=> (expenses-by-category)
{:food 42.50 :transport 15.00}

user=> (total-spending :this-month)
57.50

user=> (report :weekly)
╔════════════════════════════════════════════╗
         Weekly Expense Report               
╠════════════════════════════════════════════╣
  Food:        $42.50  (74%)                
  Transport:   $15.00  (26%)                
╠════════════════════════════════════════════╣
  Total:       $57.50                       
╚════════════════════════════════════════════╝

Implementation Hints:

Think about your data model:

;; An expense might look like:
;; {:id 1
;;  :amount 42.50
;;  :category :food
;;  :description "Lunch"
;;  :date #inst "2025-01-15"}

;; All expenses stored in a vector inside an atom:
;; (def expenses (atom []))

;; Adding an expense creates a NEW vector:
;; (swap! expenses conj new-expense)
;; The old vector still exists! It's just not referenced anymore.

;; For queries, think about:
;; - filter: keep only expenses matching criteria
;; - map: transform expenses (e.g., extract amounts)
;; - reduce: aggregate values (e.g., sum amounts)
;; - group-by: organize by category

Questions to explore:

  1. How do you generate unique IDs without mutation?
  2. How do you filter expenses by date range?
  3. What’s the difference between reduce and apply?
  4. How do you save/load data to a file in edn format?

Learning milestones:

  1. Expenses are immutable maps → You understand data modeling
  2. Queries use filter/map/reduce → You understand sequence operations
  3. State lives in an atom → You understand controlled mutation
  4. Data persists as edn → You understand serialization

Project 3: Concurrent Web Scraper with core.async

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: Clojure
  • Alternative Programming Languages: Go, Erlang, Elixir
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Concurrency / Asynchronous Programming
  • Software or Tool: core.async, clj-http, Enlive
  • Main Book: “Clojure Applied” by Alex Miller

What you’ll build: A web scraper that fetches multiple pages concurrently using core.async channels, processes them in parallel, and aggregates results—all without callbacks or promises.

Why it teaches Clojure: Clojure’s core.async brings Go-style channels and goroutines to the JVM. This project teaches you CSP (Communicating Sequential Processes)—a concurrency model where processes communicate via channels, not shared memory. It’s elegant and eliminates callback hell.

Core challenges you’ll face:

  • Understanding channels → maps to typed queues between concurrent processes
  • Go blocks vs threads → maps to lightweight vs heavyweight concurrency
  • Coordinating multiple scrapers → maps to pipeline patterns
  • Handling errors in async code → maps to error channels and supervision

Key Concepts:

  • core.async Basics: Clojure.org - core.async Guide
  • Go Blocks: “Clojure Applied” Ch. 8 - Miller
  • Channel Operations: Timothy Baldridge’s core.async talks
  • Web Scraping: Enlive documentation

Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Projects 1-2, understanding of async concepts

Real world outcome:

user=> (scrape-sites ["https://news.ycombinator.com"
                      "https://reddit.com/r/programming"
                      "https://lobste.rs"])

Starting concurrent scrape of 3 sites...
[news.ycombinator.com] Fetched in 234ms
[reddit.com] Fetched in 456ms
[lobste.rs] Fetched in 189ms

Results:
┌─────────────────────────────────────────────────────────────────┐
 Hacker News (30 articles)                                       
├─────────────────────────────────────────────────────────────────┤
 1. "Why Clojure?" - 234 points                                 
 2. "New Rust Features" - 189 points                            
 ...                                                             
├─────────────────────────────────────────────────────────────────┤
 Reddit r/programming (25 posts)                                 
├─────────────────────────────────────────────────────────────────┤
 1. "TIL about persistent data structures" - 456 upvotes        
 ...                                                             
└─────────────────────────────────────────────────────────────────┘

Total: 73 articles fetched in 456ms (parallel) vs ~879ms (sequential)

Implementation Hints:

Think about core.async as pipes and workers:

;; Channels are like typed queues:
;; (def results-chan (chan 10))  ; buffer of 10

;; Go blocks are lightweight threads:
;; (go
;;   (let [result (<! input-chan)]  ; take from channel
;;     (>! output-chan (process result))))  ; put to channel

;; Pipeline pattern:
;; URLs → [fetch workers] → HTML → [parse workers] → Data

;; Fan-out: One producer, many consumers
;; Fan-in: Many producers, one consumer

;; Use async/merge or async/into for aggregation

Questions to explore:

  1. What’s the difference between >! and >!!? When use each?
  2. How do you handle a channel that might block forever?
  3. How do you implement timeouts for slow requests?
  4. How do you close channels and handle cleanup?

Learning milestones:

  1. Single page fetch works → You understand basic HTTP
  2. Multiple pages fetch concurrently → You understand go blocks
  3. Results aggregate correctly → You understand channel operations
  4. Errors don’t crash everything → You understand error handling

Project 4: REST API with Ring and Compojure

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: Clojure
  • Alternative Programming Languages: Python/Flask, Node/Express, Ruby/Sinatra
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Web Development / HTTP
  • Software or Tool: Ring, Compojure, Leiningen
  • Main Book: “Web Development with Clojure” by Dmitri Sotnikov

What you’ll build: A RESTful API for a todo application with CRUD operations, middleware for logging and authentication, and JSON responses.

Why it teaches Clojure: Ring shows Clojure’s elegance—an HTTP request is just a map, a response is just a map, and middleware is just function composition. No magic annotations, no complex frameworks—just data and functions.

Core challenges you’ll face:

  • Understanding Ring’s request/response model → maps to HTTP as data transformation
  • Composing middleware → maps to functional composition in practice
  • Routing with Compojure → maps to declarative route definitions
  • JSON serialization → maps to data interchange

Key Concepts:

  • Ring Concepts: Ring GitHub wiki
  • Compojure Routes: Compojure documentation
  • Middleware: “Web Development with Clojure” Ch. 3 - Sotnikov
  • JSON Handling: Cheshire library documentation

Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Projects 1-3, basic HTTP knowledge

Real world outcome:

$ curl http://localhost:3000/api/todos
{"todos": [{"id": 1, "title": "Learn Clojure", "done": false}]}

$ curl -X POST http://localhost:3000/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "Master macros"}'
{"id": 2, "title": "Master macros", "done": false, "created_at": "2025-01-15T10:30:00Z"}

$ curl -X PUT http://localhost:3000/api/todos/2 \
  -d '{"done": true}'
{"id": 2, "title": "Master macros", "done": true}

$ curl http://localhost:3000/api/todos?done=true
{"todos": [{"id": 2, "title": "Master macros", "done": true}]}

Implementation Hints:

Ring is beautifully simple:

;; A Ring handler is just a function:
;; (fn [request] response)

;; Request is a map:
;; {:request-method :get
;;  :uri "/api/todos"
;;  :headers {"content-type" "application/json"}
;;  :body ...}

;; Response is a map:
;; {:status 200
;;  :headers {"Content-Type" "application/json"}
;;  :body "{\"todos\": [...]}"}

;; Middleware wraps handlers:
;; (defn wrap-logging [handler]
;;   (fn [request]
;;     (println "Request:" (:uri request))
;;     (handler request)))

;; Compojure for routing:
;; (defroutes app-routes
;;   (GET "/api/todos" [] (get-all-todos))
;;   (POST "/api/todos" {body :body} (create-todo body)))

Questions to explore:

  1. How does middleware compose? (Hint: think of Russian dolls)
  2. How do you parse JSON from the request body?
  3. How do you handle 404s and other errors consistently?
  4. How do you add CORS headers for frontend access?

Learning milestones:

  1. Hello World endpoint works → You understand Ring basics
  2. CRUD operations complete → You understand routing
  3. Middleware logs requests → You understand composition
  4. Errors return proper JSON → You understand error handling

Project 5: Macro-Powered Testing DSL

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: Clojure
  • Alternative Programming Languages: Common Lisp, Racket, Elixir
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Macros / Metaprogramming
  • Software or Tool: Leiningen, REPL
  • Main Book: “Mastering Clojure Macros” by Colin Jones

What you’ll build: A custom testing framework with a clean DSL, property-based testing support, and helpful error messages—all powered by macros.

Why it teaches Clojure: Macros are Clojure’s superpower. By building a testing DSL, you’ll understand why “code is data”—you’ll manipulate code as easily as you manipulate any other data structure. This is impossible in most languages.

Core challenges you’ll face:

  • Understanding quote and unquote → maps to syntax-quote, ~, ~@
  • Macro expansion → maps to compile-time vs runtime
  • Capturing vs generating symbols → maps to gensym and hygiene
  • Error messages → maps to macro debugging is hard

Key Concepts:

  • Macro Basics: “Clojure for the Brave and True” Ch. 8
  • Syntax-Quote: “Mastering Clojure Macros” Ch. 2 - Jones
  • Common Patterns: “Mastering Clojure Macros” Ch. 4 - Jones
  • Debugging Macros: macroexpand and macroexpand-1

Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Projects 1-4, solid Clojure fundamentals

Real world outcome:

;; Your DSL in action:
(deftest user-authentication
  (given {:user {:name "Alice" :password "secret123"}}
    (when (authenticate (:name user) (:password user))
      (should :succeed)
      (should-return {:authenticated true
                     :user-id some?})
      (should-not :throw-exception))
    
    (when (authenticate (:name user) "wrong-password")
      (should :fail)
      (should-return {:authenticated false
                     :error "Invalid credentials"}))))

(run-tests)
;; Output:
;; Testing user-authentication
;;   ✓ authenticate with correct password succeeds
;;   ✓ returns authenticated user
;;   ✓ authenticate with wrong password fails
;;   ✓ returns error message
;; 
;; 4 tests passed, 0 failed

Implementation Hints:

Macros transform code at compile time:

;; Think of a macro as a function that runs at compile time
;; and returns code (which then runs at runtime)

;; The quote (') prevents evaluation
'(+ 1 2)  ; => the list (+ 1 2), not 3

;; Syntax-quote (`) is like quote but with superpowers:
;; - Resolves symbols to their namespaces
;; - Allows unquote (~) to inject values
;; - Allows unquote-splice (~@) for lists

;; Example:
(defmacro when-let* [bindings & body]
  `(let [~@bindings]
     (when (and ~@(take-nth 2 bindings))
       ~@body)))

;; Use macroexpand-1 to see what your macro produces:
;; (macroexpand-1 '(your-macro args...))

;; gensym creates unique symbols to avoid capture:
;; (gensym "temp")  ; => temp123

Questions to explore:

  1. What’s the difference between ‘ and ` (quote vs syntax-quote)?
  2. Why do you need gensym? What’s “variable capture”?
  3. How do you pass the current line number to error messages?
  4. When should you use a macro vs a function?

Learning milestones:

  1. Simple assertion macro works → You understand basic macros
  2. DSL syntax feels natural → You understand syntax design
  3. Error messages include line numbers → You understand metadata
  4. No variable capture issues → You understand hygiene

Project 6: ClojureScript Single-Page Application

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: ClojureScript
  • Alternative Programming Languages: Elm, PureScript, ReasonML
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Frontend Development / React Wrappers
  • Software or Tool: Shadow-cljs, Reagent, Re-frame
  • Main Book: “Web Development with Clojure” by Dmitri Sotnikov

What you’ll build: A reactive todo application with Reagent (React wrapper) and Re-frame (state management), featuring real-time updates and persistent storage.

Why it teaches Clojure: ClojureScript brings Clojure’s power to the browser. With Reagent, you write React components as simple Clojure functions. With Re-frame, you get Redux-like state management that’s actually pleasant to use. No JavaScript fatigue—just Clojure everywhere.

Core challenges you’ll face:

  • Reagent components → maps to hiccup syntax, reactive atoms
  • Re-frame subscriptions → maps to derived state, efficiency
  • Re-frame events → maps to pure event handlers
  • JavaScript interop → maps to calling JS from CLJS

Key Concepts:

  • Reagent Basics: Reagent documentation
  • Re-frame Architecture: Re-frame documentation
  • Hiccup Syntax: Hiccup GitHub
  • Shadow-cljs: Shadow-cljs User Guide

Difficulty: Intermediate Time estimate: 2 weeks Prerequisites: Projects 1-4, basic HTML/CSS

Real world outcome:

;; Your ClojureScript app in action:
;; Browser shows a beautiful todo app

;; Adding a todo via the UI updates instantly
;; Completing a todo shows strikethrough
;; Filter buttons (All/Active/Completed) work
;; Data persists in localStorage

;; The code is elegant:
(defn todo-item [{:keys [id title done]}]
  [:li {:class (when done "completed")}
   [:input {:type "checkbox"
           :checked done
           :on-change #(rf/dispatch [:toggle-todo id])}]
   [:span title]
   [:button {:on-click #(rf/dispatch [:delete-todo id])} "×"]])

(defn todo-list []
  (let [todos @(rf/subscribe [:visible-todos])]
    [:ul.todo-list
     (for [todo todos]
       ^{:key (:id todo)} [todo-item todo])]))

Implementation Hints:

Reagent uses hiccup—HTML as data:

;; Hiccup syntax:
;; [:tag.class#id {:attr "value"} children...]

[:div.container
 [:h1 "Todos"]
 [:ul
  [:li "Item 1"]
  [:li "Item 2"]]]

;; Reagent atoms are reactive:
(def counter (r/atom 0))

(defn counter-component []
  [:div
   [:p "Count: " @counter]
   [:button {:on-click #(swap! counter inc)} "+1"]])
;; The component re-renders when counter changes!

;; Re-frame pattern:
;; View → dispatch event → handler updates db → subscription → view
;;
;; (rf/dispatch [:add-todo "New item"])  ; fire event
;; (rf/subscribe [:all-todos])           ; read state

Questions to explore:

  1. How does Reagent know when to re-render?
  2. What’s the difference between r/atom and re-frame’s app-db?
  3. How do you call a JavaScript library from ClojureScript?
  4. How do you handle async operations in Re-frame?

Learning milestones:

  1. Static component renders → You understand hiccup
  2. Component updates on state change → You understand reactivity
  3. Re-frame events flow correctly → You understand the architecture
  4. App persists and reloads data → You understand effects

Project 7: Multi-threaded Data Pipeline with Transducers

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: Clojure
  • Alternative Programming Languages: Rust (iterators), Haskell
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Data Processing / Performance
  • Software or Tool: Clojure core, Criterium for benchmarks
  • Main Book: “Clojure Applied” by Alex Miller

What you’ll build: A high-performance log processing pipeline that uses transducers for efficient data transformation, parallel processing for speed, and reducers for aggregation.

Why it teaches Clojure: Transducers are one of Clojure’s most elegant innovations—they separate the “what” (transformation) from the “how” (execution context). The same transducer works on collections, channels, and observables. This project teaches you to think about data flow abstractly.

Core challenges you’ll face:

  • Understanding transducers → maps to reusable transformations
  • Composing transducers → maps to efficient pipelines
  • Parallel processing → maps to reducers and pmap
  • Performance optimization → maps to avoiding intermediate collections

Key Concepts:

  • Transducers: Clojure.org - Transducers Reference
  • Reducers: “Clojure Applied” Ch. 7 - Miller
  • Performance: Criterium for benchmarking
  • Core Functions: Rich Hickey’s “Transducers” talk

Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 1-5, comfortable with higher-order functions

Real world outcome:

user=> (process-logs "/var/log/nginx/access.log"
                     {:filter-status 500
                      :aggregate-by :endpoint
                      :parallel? true})

Processing 10,000,000 log entries...
Using 8 cores for parallel processing

Results:
┌─────────────────────────────────────────────────────────────────┐
 Endpoint                     500 Errors   % of Total         
├─────────────────────────────────────────────────────────────────┤
 /api/users                      15,234      0.15%            
 /api/orders                      8,456      0.08%            
 /api/payments                    3,211      0.03%            
└─────────────────────────────────────────────────────────────────┘

Performance:
  Sequential: 45.2 seconds
  Parallel:   8.3 seconds  (5.4x speedup)
  
Memory:
  Without transducers: 2.3 GB peak
  With transducers:    256 MB peak  (9x reduction)

Implementation Hints:

Transducers separate transformation from execution:

;; Regular sequence operations create intermediate collections:
(->> data
     (filter pred1)   ; creates seq
     (map f)          ; creates another seq
     (take n))        ; creates yet another seq

;; Transducers compose transformations without intermediates:
(def xf (comp
          (filter pred1)
          (map f)
          (take n)))

;; Apply to different contexts:
(into [] xf data)           ; to vector
(transduce xf + 0 data)     ; reduce with +
(sequence xf data)          ; lazy sequence
(chan 10 xf)                ; core.async channel

;; For parallel processing, use reducers:
(require '[clojure.core.reducers :as r])
(r/fold + (r/filter pred (r/map f data)))

Questions to explore:

  1. Why are transducers more efficient than chained sequence operations?
  2. How does comp work with transducers (hint: it’s backwards)?
  3. When should you use pmap vs r/fold?
  4. How do you write a custom transducer?

Learning milestones:

  1. Basic transducer works → You understand the concept
  2. Composed transducers process data → You understand composition
  3. Pipeline runs in parallel → You understand reducers
  4. Performance measurably improves → You understand optimization

Project 8: Full-Stack App with Spec Validation

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: Clojure/ClojureScript
  • Alternative Programming Languages: TypeScript, Haskell
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Type Systems / Data Validation
  • Software or Tool: clojure.spec, Ring, Reagent
  • Main Book: “Clojure Applied” by Alex Miller

What you’ll build: A full-stack user management system where clojure.spec validates data on both frontend and backend, generates tests automatically, and provides helpful error messages.

Why it teaches Clojure: Clojure.spec is Clojure’s answer to static types—but more flexible. It’s a way to describe the shape of your data and functions, then use those descriptions for validation, testing, documentation, and error messages. It brings the benefits of types without the rigidity.

Core challenges you’ll face:

  • Writing specs → maps to describing data shapes
  • Function specs → maps to describing inputs/outputs
  • Generative testing → maps to automatic test generation
  • Sharing specs → maps to full-stack validation

Key Concepts:

  • Spec Basics: Clojure.org - Spec Guide
  • Generative Testing: test.check documentation
  • Function Specs: Clojure.org - Spec and Functions
  • Spec in Practice: “Clojure Applied” Ch. 4 - Miller

Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Projects 1-6

Real world outcome:

;; Define specs once, use everywhere:
(s/def ::email (s/and string? #(re-matches #".+@.+\..+" %)))
(s/def ::age (s/int-in 0 150))
(s/def ::user (s/keys :req-un [::email ::age ::name]))

;; Validation on backend:
(s/valid? ::user {:email "alice@example.com" :age 30 :name "Alice"})
; => true

(s/explain ::user {:email "not-an-email" :age -5})
; In: [:email] val: "not-an-email" fails spec: :app/email
; In: [:age] val: -5 fails spec: :app/age predicate: (int-in 0 150)

;; Generate test data automatically:
(gen/sample (s/gen ::user) 3)
; => ({:email "a@b.c" :age 23 :name "Xk"}
;     {:email "test@domain.org" :age 45 :name "Alice"}
;     ...)

;; Same specs work in ClojureScript frontend!

Implementation Hints:

Spec is about describing data:

;; Primitives:
(s/def ::positive-int (s/and int? pos?))
(s/def ::non-empty-string (s/and string? not-empty))

;; Collections:
(s/def ::names (s/coll-of string? :min-count 1))
(s/def ::scores (s/map-of keyword? int?))

;; Maps (records):
(s/def ::person
  (s/keys :req-un [::name ::email]    ; required
          :opt-un [::phone ::age]))   ; optional

;; Functions:
(s/fdef calculate-tax
  :args (s/cat :income ::positive-int :rate ::percentage)
  :ret ::positive-int
  :fn #(< (:ret %) (-> % :args :income)))

;; Use with instrument for runtime checking:
(stest/instrument `calculate-tax)

Questions to explore:

  1. What’s the difference between :req and :req-un?
  2. How do you create custom generators?
  3. How do you share specs between Clojure and ClojureScript?
  4. When is spec better than static types? When is it worse?

Learning milestones:

  1. Basic specs validate data → You understand spec primitives
  2. Specs generate test data → You understand generators
  3. Function specs catch errors → You understand instrumentation
  4. Same specs work full-stack → You understand sharing

Project 9: Software Transactional Memory Bank Simulation

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: Clojure
  • Alternative Programming Languages: Haskell (STM), Scala
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Concurrency / Transactions
  • Software or Tool: Clojure refs, dosync
  • Main Book: “Programming Clojure” by Halloway & Bedra

What you’ll build: A banking system simulation where transfers between accounts are atomic, consistent, and isolated—using Clojure’s STM (Software Transactional Memory) instead of locks.

Why it teaches Clojure: STM is Clojure’s answer to the problems of locks. Instead of manually coordinating access to shared state, you describe transactions declaratively and the runtime ensures they’re atomic. It’s like database transactions for in-memory data. This project teaches you why immutability + STM = fearless concurrency.

Core challenges you’ll face:

  • Understanding refs → maps to coordinated, synchronous state
  • Transactions with dosync → maps to atomic operations
  • Handling contention → maps to automatic retry
  • Ensuring consistency → maps to validators and watches

Key Concepts:

  • Refs and Transactions: “Programming Clojure” Ch. 6
  • STM Concepts: Clojure.org - Refs and Transactions
  • Contention: Rich Hickey’s “Are We There Yet?” talk
  • Validators: Clojure.org - Refs

Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 1-4, understanding of concurrency issues

Real world outcome:

user=> (def bank (create-bank {:alice 1000 :bob 500 :charlie 750}))
#'user/bank

user=> (transfer! bank :alice :bob 200)
{:success true :alice 800 :bob 700}

;; Concurrent transfers (run 1000 transfers in parallel)
user=> (stress-test bank 1000)
Running 1000 concurrent random transfers...

Results:
  Successful transfers: 1000
  Failed transfers: 0
  Retries due to contention: 47
  
  Initial total: $2250
  Final total:   $2250   (money is conserved!)
  
  Account balances:
    :alice   -> $847
    :bob     -> $923
    :charlie -> $480

;; Try to overdraw (fails atomically)
user=> (transfer! bank :alice :bob 1000)
{:success false :error "Insufficient funds" :alice 847 :bob 923}
;; Alice's balance unchanged—transaction rolled back

Implementation Hints:

STM makes concurrent state changes safe:

;; Refs are for coordinated state:
(def account-a (ref 1000))
(def account-b (ref 500))

;; dosync creates a transaction:
(dosync
  (alter account-a - 100)  ; deduct from A
  (alter account-b + 100)) ; add to B
;; Either BOTH happen or NEITHER happens

;; If another transaction conflicts, one is retried automatically
;; You never see inconsistent intermediate states

;; Validators prevent invalid states:
(def account (ref 0 :validator #(>= % 0)))
(dosync (alter account - 100))  ; throws—would go negative

;; Watches observe state changes:
(add-watch account :logger
  (fn [key ref old new]
    (println "Balance changed from" old "to" new)))

Questions to explore:

  1. What happens if a transaction reads a ref that changes?
  2. How does retry work? Can you observe partial state?
  3. When would you use commute instead of alter?
  4. How do you handle transactions that have side effects?

Learning milestones:

  1. Single transfer works → You understand basic refs
  2. Concurrent transfers are safe → You understand transactions
  3. Overdrafts are prevented → You understand validators
  4. System handles high contention → You understand retry

Project 10: Datomic-Style Database from Scratch

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: Clojure
  • Alternative Programming Languages: None (Clojure-specific concepts)
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 4: Expert
  • Knowledge Area: Databases / Datalog
  • Software or Tool: Datascript, or implement from scratch
  • Main Book: “Clojure Programming” by Emerick, Carper & Grand

What you’ll build: A simple immutable database with Datalog queries, time-travel (point-in-time queries), and entity-attribute-value storage—inspired by Datomic.

Why it teaches Clojure: Datomic is Rich Hickey’s vision of what a database should be—immutable, where every fact is timestamped, and you can query the past. Building a mini-version teaches you advanced Clojure patterns: protocols, persistent data structures, and the power of data-oriented design.

Core challenges you’ll face:

  • EAV storage model → maps to entity-attribute-value triples
  • Datalog query engine → maps to pattern matching, unification
  • Immutable history → maps to persistent storage of facts
  • Indexing → maps to efficient query execution

Key Concepts:

  • EAV Model: Datomic documentation
  • Datalog: “Learn Datalog Today” (learndatalogtoday.org)
  • Persistent Data Structures: “Clojure Programming” Ch. 4
  • Protocols: Clojure.org - Protocols

Difficulty: Expert Time estimate: 3-4 weeks Prerequisites: Projects 1-7, data structure knowledge

Real world outcome:

;; Add facts to the database
user=> (transact! db
        [{:person/name "Alice" :person/age 30}
         {:person/name "Bob" :person/age 25}])

;; Query with Datalog
user=> (q '[:find ?name ?age
           :where [?e :person/name ?name]
                  [?e :person/age ?age]
                  [(> ?age 20)]]
         db)
#{["Alice" 30] ["Bob" 25]}

;; Update Alice's age
user=> (transact! db
        [{:db/id [:person/name "Alice"] :person/age 31}])

;; Query the past!
user=> (q '[:find ?age :where [?e :person/name "Alice"]
                              [?e :person/age ?age]]
         (as-of db yesterday))
#{[30]}  ; Alice was 30 yesterday

user=> (q '[:find ?age :where [?e :person/name "Alice"]
                              [?e :person/age ?age]]
         db)
#{[31]}  ; Alice is 31 now

;; See the full history
user=> (history-of db [:person/name "Alice"] :person/age)
[{:value 30 :added-at #inst "2025-01-14T10:00:00"}
 {:value 31 :added-at #inst "2025-01-15T10:00:00"}]

Implementation Hints:

Think about facts as immutable data:

;; A datom (fact) is: [entity attribute value time added?]
;; [:e1 :person/name "Alice" 1000 true]
;; [:e1 :person/age 30 1000 true]
;; [:e1 :person/age 31 1001 true]   ; new value
;; [:e1 :person/age 30 1001 false]  ; old value retracted

;; Storage is just a sorted set of datoms
;; Different indexes sort differently:
;; - EAVT: find all attributes of an entity
;; - AEVT: find all entities with an attribute
;; - AVET: find entities by attribute value

;; Datalog query engine:
;; 1. Parse query into patterns
;; 2. For each pattern, find matching datoms
;; 3. Unify variables across patterns (join)
;; 4. Return matching bindings

;; Key insight: The database at time T is just
;; a filtered view of all datoms where time <= T

Questions to explore:

  1. How do you efficiently index EAV triples?
  2. How does Datalog unification work?
  3. How do you represent “retraction” of a fact?
  4. How do you ensure transaction atomicity?

Learning milestones:

  1. Add and retrieve facts → You understand EAV
  2. Simple queries work → You understand pattern matching
  3. Joins work correctly → You understand unification
  4. Time-travel queries work → You understand immutable history

Project 11: EDN Configuration Management System

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: Clojure
  • Alternative Programming Languages: YAML/JSON tools, Terraform
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Configuration / DevOps
  • Software or Tool: Aero, Integrant, Mount
  • Main Book: “Clojure Applied” by Alex Miller

What you’ll build: A configuration management system that reads EDN configs, supports environment-specific overrides, validates with spec, and manages component lifecycles.

Why it teaches Clojure: EDN (Extensible Data Notation) is Clojure’s data format—like JSON but with more types and extensibility. This project teaches you how Clojure programs are configured and how component systems manage stateful resources.

Core challenges you’ll face:

  • Reading EDN files → maps to native data format
  • Environment overrides → maps to merging configurations
  • Spec validation → maps to ensuring valid config
  • Component lifecycle → maps to start/stop order

Key Concepts:

  • EDN Format: github.com/edn-format/edn
  • Aero Library: Juxt Aero documentation
  • Integrant: Integrant GitHub
  • Component Pattern: Stuart Sierra’s “Component” library

Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Projects 1-4, 8

Real world outcome:

;; config/base.edn
{:db {:host "localhost"
      :port 5432
      :name "myapp"}
 :http {:port 3000}
 :cache {:ttl-seconds 300}}

;; config/prod.edn (overrides base)
{:db {:host #env "DB_HOST"
      :password #env "DB_PASSWORD"}
 :http {:port #env ["PORT" :int 8080]}}

;; In your app:
user=> (def config (load-config :prod))
user=> config
{:db {:host "prod-db.example.com" :port 5432 :name "myapp" :password "***"}
 :http {:port 8080}
 :cache {:ttl-seconds 300}}

;; Validation
user=> (validate-config config)
 Config is valid

;; Component lifecycle
user=> (def system (start-system config))
Starting :db...
Starting :http (depends on :db)...
Starting :cache...
System started.

user=> (stop-system system)
Stopping :cache...
Stopping :http...
Stopping :db...
System stopped.

Implementation Hints:

EDN is data, so configuration is just data:

;; EDN readers for custom tags:
;; #env - read environment variable
;; #include - include another file
;; #profile - select based on profile

(defn env-reader [value]
  (cond
    (string? value) (System/getenv value)
    (vector? value) (let [[var type default] value]
                      (or (parse (System/getenv var) type) default))))

;; Register the reader:
(edn/read-string {:readers {'env env-reader}}
                 "#env \"HOME\"")

;; Component lifecycle with protocols:
(defprotocol Lifecycle
  (start [this config])
  (stop [this]))

;; Dependency order:
;; Build a dependency graph, topological sort for start order
;; Reverse for stop order

Questions to explore:

  1. How do you handle sensitive values in configs?
  2. How do you validate that all required env vars exist?
  3. How do you handle circular dependencies?
  4. How do you support config reloading without restart?

Learning milestones:

  1. Read basic EDN config → You understand the format
  2. Environment overrides work → You understand merging
  3. Validation catches errors → You understand spec
  4. Components start/stop correctly → You understand lifecycle

Project 12: Clojure REPL Debugging Tools

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: Clojure
  • Alternative Programming Languages: Common Lisp (SLIME), Smalltalk
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Developer Tools / REPL
  • Software or Tool: nREPL, CIDER, Calva
  • Main Book: “Living Clojure” by Carin Meier

What you’ll build: A suite of REPL debugging utilities including a function tracer, an interactive data inspector, and a hot-reload system for development.

Why it teaches Clojure: The REPL is Clojure’s killer feature. This project teaches you to extend the REPL with your own tools—you’ll understand how Clojure’s dynamic nature enables powerful introspection and modification of running programs.

Core challenges you’ll face:

  • Function tracing → maps to alter-var-root, with-redefs
  • Data inspection → maps to pretty printing, navigation
  • Hot reloading → maps to namespace reloading, state preservation
  • REPL middleware → maps to nREPL architecture

Key Concepts:

  • Vars and Namespaces: Clojure.org - Vars
  • Dynamic Bindings: “Clojure Programming” Ch. 10
  • nREPL: nREPL documentation
  • Tools.namespace: tools.namespace GitHub

Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Projects 1-4

Real world outcome:

;; Trace function calls:
user=> (trace #'my-function)
Tracing my-function

user=> (my-function {:a 1 :b 2})
TRACE my-function called with: {:a 1, :b 2}
TRACE my-function returned: 3 (took 0.5ms)
3

;; Inspect data interactively:
user=> (inspect large-nested-map)
┌─────────────────────────────────────────────────────────┐
 {:users [...(50 items)...]                             
  :config {:db {...} :http {...}}                       
  :cache {...}}                                          
└─────────────────────────────────────────────────────────┘
Commands: (n)ext, (p)rev, (d)rill-down, (u)p, (q)uit
> d users
[{:id 1, :name "Alice", :email "alice@example.com", ...}
 {:id 2, :name "Bob", ...}
 ...]

;; Hot reload with state preservation:
user=> (reload!)
Reloading: app.core, app.handlers, app.db...
Preserving state: db-connection, cache
Reload complete. 3 namespaces reloaded.

Implementation Hints:

Clojure’s dynamism enables powerful tooling:

;; Trace a function by wrapping it:
(defn trace [var]
  (let [original @var
        traced (fn [& args]
                 (println "Called with:" args)
                 (let [result (apply original args)]
                   (println "Returned:" result)
                   result))]
    (alter-var-root var (constantly traced))))

;; Save original to restore:
(def ^:dynamic *traced-originals* {})

;; Hot reloading with tools.namespace:
(require '[clojure.tools.namespace.repl :as repl])
(repl/refresh)  ; reloads changed namespaces

;; Preserve state during reload:
(defonce db-connection (connect!))  ; defonce survives reload

Questions to explore:

  1. How do you undo tracing? (Hint: save the original)
  2. How do you trace all functions in a namespace?
  3. How do you detect which namespaces have changed?
  4. How do you handle reload errors gracefully?

Learning milestones:

  1. Function tracing works → You understand vars
  2. Data inspector navigates → You understand data
  3. Hot reload preserves state → You understand namespaces
  4. Tools integrate with REPL → You understand nREPL

Project 13: Property-Based Testing Framework

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: Clojure
  • Alternative Programming Languages: Haskell (QuickCheck), Scala (ScalaCheck)
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Testing / Formal Methods
  • Software or Tool: test.check, spec
  • Main Book: “Clojure Applied” by Alex Miller

What you’ll build: A property-based testing library that generates random test cases, shrinks failing cases to minimal examples, and integrates with clojure.spec.

Why it teaches Clojure: Property-based testing is a paradigm shift—instead of writing specific test cases, you describe properties that should always hold, and the computer generates thousands of test cases. This project teaches you to think about invariants and leverages Clojure’s spec for data generation.

Core challenges you’ll face:

  • Random generation → maps to generators for all data types
  • Property definition → maps to what should always be true?
  • Shrinking → maps to finding minimal failing case
  • Spec integration → maps to s/gen, s/exercise

Key Concepts:

  • test.check: test.check documentation
  • Generators: “Clojure Applied” Ch. 5 - Miller
  • Shrinking: QuickCheck papers
  • Property Thinking: John Hughes’ talks

Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Projects 1-8

Real world outcome:

;; Define properties that must always hold:
(defproperty reverse-reverse-is-identity
  [v (gen/vector gen/int)]
  (= v (reverse (reverse v))))

(defproperty sort-is-idempotent
  [v (gen/vector gen/int)]
  (= (sort v) (sort (sort v))))

(defproperty sort-preserves-elements
  [v (gen/vector gen/int)]
  (= (frequencies v) (frequencies (sort v))))

user=> (check! reverse-reverse-is-identity)
 Passed 1000 tests

user=> (check! my-buggy-function-property)
 Failed after 42 tests

  Failing input: [3 1 4 1 5 9]
  
  Shrunk to minimal case:
  Failing input: [1 0]
  
  Expected: true
  Actual:   false

Implementation Hints:

Property-based testing inverts your thinking:

;; Instead of:
(deftest test-reverse
  (is (= [3 2 1] (reverse [1 2 3]))))

;; Think about properties:
;; - reverse of reverse is identity
;; - reverse preserves length
;; - first of original is last of reversed

;; Generators create random data:
(gen/sample gen/int 5)          ; => (0 1 -1 3 -2)
(gen/sample gen/string 3)       ; => ("" "a" "Gg")
(gen/sample (gen/vector gen/int) 3)  ; => ([] [0] [1 -1])

;; Combine generators:
(def gen-user
  (gen/hash-map
    :name gen/string-alphanumeric
    :age (gen/choose 0 120)
    :email (gen/fmap #(str % "@example.com") 
                     gen/string-alphanumeric)))

;; Shrinking finds minimal failing case:
;; [3 1 4 1 5 9] fails
;; Try [3 1 4 1 5] - fails
;; Try [3 1 4 1] - fails
;; Try [3 1] - passes
;; Try [1 4 1] - fails
;; Try [1 4] - fails
;; Try [1 0] - fails (minimal!)

Questions to explore:

  1. How do you write a generator for your domain types?
  2. What makes a good property?
  3. How does shrinking work for complex data?
  4. How do you test stateful systems with properties?

Learning milestones:

  1. Basic generators work → You understand generation
  2. Properties detect bugs → You understand invariants
  3. Shrinking finds minimal case → You understand shrinking
  4. Spec-generated tests work → You understand integration

Project 14: Clojure-to-JavaScript Compiler (Mini)

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: Clojure
  • Alternative Programming Languages: Scheme, Racket
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 5: Master
  • Knowledge Area: Compilers / Language Implementation
  • Software or Tool: tools.analyzer, tools.emitter
  • Main Book: “Writing a C Compiler” by Nora Sandler (concepts transfer)

What you’ll build: A mini Clojure-to-JavaScript compiler that handles a subset of Clojure (basic expressions, functions, let bindings, if) and produces runnable JavaScript.

Why it teaches Clojure: ClojureScript is a production-quality Clojure-to-JS compiler. Building a mini version teaches you how code transformation works at a fundamental level—you’ll implement the core of what makes Clojure work.

Core challenges you’ll face:

  • Parsing S-expressions → maps to read-string, syntax
  • AST representation → maps to code as data
  • Code generation → maps to emitting JavaScript
  • Interop emission → maps to calling JS from Clojure

Key Concepts:

  • Reader: Clojure Reader documentation
  • tools.analyzer: tools.analyzer GitHub
  • Code Generation: Compiler design books
  • JavaScript AST: ESTree specification

Difficulty: Master Time estimate: 4-6 weeks Prerequisites: All previous projects, compiler basics

Real world outcome:

;; Your mini compiler in action:
user=> (compile-to-js '(defn add [a b] (+ a b)))
"function add(a, b) { return (a + b); }"

user=> (compile-to-js '(let [x 10 y 20] (+ x y)))
"(function() { var x = 10; var y = 20; return (x + y); })()"

user=> (compile-to-js '(if (> x 0) "positive" "non-positive"))
"((x > 0) ? \"positive\" : \"non-positive\")"

user=> (compile-to-js '(map inc [1 2 3]))
"cljs.core.map(cljs.core.inc, [1, 2, 3])"

;; Full program:
user=> (compile-program 
        '[(defn factorial [n]
            (if (<= n 1)
              1
              (* n (factorial (dec n)))))
          (println (factorial 5))])
"function factorial(n) {
  return (n <= 1) ? 1 : (n * factorial(n - 1));
}
console.log(factorial(5));"

Implementation Hints:

Compilation is code transformation:

;; Clojure's reader gives you AST for free:
(read-string "(+ 1 2)")  ; => (+ 1 2)

;; The list IS the AST:
;; (+ 1 2) means: call + with args 1 and 2

;; Compilation is pattern matching on the AST:
(defmulti emit (fn [form] (type form)))

(defmethod emit clojure.lang.Symbol [s]
  (munge (str s)))  ; handle special chars

(defmethod emit java.lang.Long [n]
  (str n))

(defmethod emit clojure.lang.PersistentList [[op & args]]
  (case op
    'if (emit-if args)
    'let (emit-let args)
    'fn (emit-fn args)
    'defn (emit-defn args)
    ;; default: function call
    (emit-call op args)))

(defn emit-if [[test then else]]
  (format "(%s ? %s : %s)"
          (emit test) (emit then) (emit else)))

Questions to explore:

  1. How do you handle special forms vs function calls?
  2. How do you implement lexical scoping in JS?
  3. How do you handle Clojure’s immutable vectors in JS?
  4. How do you compile macros? (Hint: expand first)

Learning milestones:

  1. Arithmetic compiles → You understand basic emission
  2. Functions compile → You understand JS function syntax
  3. Let bindings compile → You understand scoping
  4. Recursion works → You have a working compiler

Project 15: Full-Stack Real-Time Application

  • File: LEARN_CLOJURE_DEEP_DIVE.md
  • Main Programming Language: Clojure/ClojureScript
  • Alternative Programming Languages: Elixir/Phoenix, Node/Socket.io
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 4: Expert
  • Knowledge Area: Full-Stack / Real-Time Systems
  • Software or Tool: Ring, Sente, Reagent, Re-frame
  • Main Book: “Web Development with Clojure” by Dmitri Sotnikov

What you’ll build: A collaborative whiteboard application with real-time synchronization, where multiple users can draw simultaneously and see each other’s changes instantly.

Why it teaches Clojure: This capstone project combines everything: backend with Ring, frontend with ClojureScript, WebSockets with Sente, state management with Re-frame, and concurrency with core.async. It’s a full demonstration of Clojure’s power for modern applications.

Core challenges you’ll face:

  • WebSocket communication → maps to Sente channels
  • Shared state synchronization → maps to CRDTs or OT
  • Frontend rendering → maps to canvas with Reagent
  • Concurrent updates → maps to handling conflicts

Key Concepts:

  • Sente: Sente GitHub documentation
  • WebSockets: Ring-jetty-adapter WebSocket support
  • CRDTs: Martin Kleppmann’s papers
  • Canvas Rendering: HTML5 Canvas API

Difficulty: Expert Time estimate: 4-6 weeks Prerequisites: All previous projects

Real world outcome:

╔═══════════════════════════════════════════════════════════════════╗
║                    Collaborative Whiteboard                        ║
╠═══════════════════════════════════════════════════════════════════╣
║                                                                    ║
║     [Alice's cursor]  ●------ drawing line ------●                ║
║                                                                    ║
║                         [Bob's cursor]                             ║
║                              │                                     ║
║                              │ drawing                             ║
║                              ▼                                     ║
║                              ●                                     ║
║                                                                    ║
╠═══════════════════════════════════════════════════════════════════╣
║ Connected: Alice, Bob, Charlie    │    Latency: 23ms              ║
╚═══════════════════════════════════════════════════════════════════╝

Features:
- Real-time cursor positions
- Drawing synchronization
- Undo/redo (per user and global)
- Shape tools (line, rectangle, circle)
- Export to PNG
- Replay session history

Implementation Hints:

Real-time sync is about event streams:

;; Backend: Sente channel
(defonce channel-socket (sente/make-channel-socket! ...))

;; Handle incoming events
(defmethod handle-event :draw/stroke
  [{:keys [?data uid]}]
  (let [{:keys [points color]} ?data]
    ;; Broadcast to all connected clients
    (doseq [other-uid (connected-uids)]
      (when (not= other-uid uid)
        (send! other-uid [:draw/stroke ?data])))))

;; Frontend: dispatch events to server
(defn handle-draw [event]
  (let [point (extract-point event)]
    (swap! current-stroke conj point)
    (sente/send! [:draw/point {:point point}])))

;; Optimistic updates:
;; - Draw locally immediately
;; - Send to server
;; - Server broadcasts to others
;; - On conflict, server is source of truth

Questions to explore:

  1. How do you handle users joining mid-session?
  2. How do you sync initial state efficiently?
  3. How do you handle network disconnections?
  4. How do you implement undo across multiple users?

Learning milestones:

  1. Basic WebSocket works → You understand Sente
  2. Drawing syncs between users → You understand broadcast
  3. Late joiners see history → You understand state sync
  4. Conflicts resolve correctly → You understand consistency

Project Comparison Table

# Project Difficulty Time Key Skill Fun
1 REPL Calculator Weekend Syntax, REPL ⭐⭐⭐
2 Expense Tracker 1 week Immutability ⭐⭐⭐
3 Web Scraper ⭐⭐ 1-2 weeks core.async ⭐⭐⭐⭐
4 REST API ⭐⭐ 1-2 weeks Web Development ⭐⭐⭐⭐
5 Testing DSL ⭐⭐⭐ 2-3 weeks Macros ⭐⭐⭐⭐⭐
6 ClojureScript SPA ⭐⭐ 2 weeks Frontend ⭐⭐⭐⭐
7 Data Pipeline ⭐⭐⭐ 2 weeks Transducers ⭐⭐⭐⭐
8 Full-Stack with Spec ⭐⭐⭐ 2-3 weeks Validation ⭐⭐⭐⭐
9 STM Bank ⭐⭐⭐ 2 weeks Concurrency ⭐⭐⭐⭐⭐
10 Mini Datomic ⭐⭐⭐⭐ 3-4 weeks Databases ⭐⭐⭐⭐⭐
11 Config Management ⭐⭐ 1-2 weeks EDN, Lifecycle ⭐⭐⭐
12 REPL Tools ⭐⭐ 1-2 weeks Tooling ⭐⭐⭐⭐
13 Property Testing ⭐⭐⭐ 2-3 weeks Testing ⭐⭐⭐⭐
14 Mini Compiler ⭐⭐⭐⭐⭐ 4-6 weeks Compilers ⭐⭐⭐⭐⭐
15 Real-Time App ⭐⭐⭐⭐ 4-6 weeks Full-Stack ⭐⭐⭐⭐⭐

Phase 1: Foundations (3-4 weeks)

Build your Clojure intuition:

  1. Project 1: REPL Calculator - Learn syntax and REPL workflow
  2. Project 2: Expense Tracker - Understand immutability and data modeling
  3. Project 11: Config Management - Learn EDN and namespaces

Phase 2: Core Skills (4-6 weeks)

Master the essentials:

  1. Project 4: REST API - Web development fundamentals
  2. Project 3: Web Scraper - core.async and concurrency
  3. Project 12: REPL Tools - Deep REPL understanding

Phase 3: Advanced Concepts (6-8 weeks)

Push into powerful features:

  1. Project 5: Testing DSL - Master macros
  2. Project 7: Data Pipeline - Transducers and performance
  3. Project 9: STM Bank - Advanced concurrency

Phase 4: Full-Stack Development (4-6 weeks)

Build complete applications:

  1. Project 6: ClojureScript SPA - Frontend development
  2. Project 8: Full-Stack with Spec - Validation everywhere

Phase 5: Mastery (8-12 weeks)

Capstone projects:

  1. Project 13: Property Testing - Advanced testing
  2. Project 10: Mini Datomic - Database internals
  3. Project 14: Mini Compiler - Language implementation
  4. Project 15: Real-Time App - Everything together

Summary

# Project Main Language
1 REPL Calculator & Unit Converter Clojure
2 Expense Tracker with Immutable Data Clojure
3 Concurrent Web Scraper Clojure
4 REST API with Ring/Compojure Clojure
5 Macro-Powered Testing DSL Clojure
6 ClojureScript Single-Page App ClojureScript
7 Data Pipeline with Transducers Clojure
8 Full-Stack App with Spec Clojure/ClojureScript
9 STM Bank Simulation Clojure
10 Datomic-Style Database Clojure
11 EDN Configuration System Clojure
12 REPL Debugging Tools Clojure
13 Property-Based Testing Clojure
14 Clojure-to-JS Compiler Clojure
15 Full-Stack Real-Time App Clojure/ClojureScript

Resources

Essential Books

  • “Clojure for the Brave and True” by Daniel Higginbotham - Fun, beginner-friendly introduction
  • “Programming Clojure” by Alex Miller, Stuart Halloway, Aaron Bedra - Comprehensive reference
  • “Clojure Applied” by Ben Vandgrift & Alex Miller - Real-world patterns
  • “Living Clojure” by Carin Meier - Practical learning approach
  • “Web Development with Clojure” by Dmitri Sotnikov - Full-stack web apps
  • “Mastering Clojure Macros” by Colin Jones - Deep dive into macros
  • “Getting Clojure” by Russ Olsen - Idiomatic Clojure

Online Resources

  • Clojure.org: https://clojure.org/ - Official documentation
  • ClojureDocs: https://clojuredocs.org/ - Community examples
  • 4Clojure: https://4clojure.oxal.org/ - Practice problems
  • Clojure Koans: http://clojurekoans.com/ - Learning exercises
  • Eric Normand’s Blog: https://ericnormand.me/ - Excellent tutorials

Video Resources

  • Rich Hickey’s Talks: “Simple Made Easy”, “Are We There Yet?”, “The Value of Values”
  • ClojureTV: YouTube channel with conference talks
  • Lambda Island: https://lambdaisland.com/ - Screencasts

Tools

  • Leiningen: https://leiningen.org/ - Build tool
  • Clojure CLI/deps.edn: Official Clojure CLI
  • Shadow-cljs: https://shadow-cljs.github.io/ - ClojureScript build
  • Calva: VS Code extension for Clojure
  • CIDER: Emacs package for Clojure

Libraries

  • Ring: HTTP server abstraction
  • Compojure/Reitit: Routing
  • Reagent: React wrapper
  • Re-frame: State management
  • core.async: CSP-style concurrency
  • Sente: WebSockets
  • clojure.spec: Data specification

Total Estimated Time: 6-9 months of dedicated study

After completion: You’ll think differently about programming. Immutability, functional composition, and data-oriented design will become second nature. You’ll understand why Rich Hickey insists on simplicity over easiness, and you’ll have the skills to build robust, concurrent, maintainable systems. Most importantly, you’ll never look at parentheses the same way again—you’ll see them as beautiful.