Skip to content
Clojure Programming Language Guide — Lisp on the JVM

Clojure Programming Language Guide — Lisp on the JVM

DodaTech Updated Jun 7, 2026 9 min read

Clojure is a modern Lisp dialect that runs on the Java Virtual Machine — combining functional programming with immutable data structures, a rich set of concurrency primitives, and seamless access to the entire Java ecosystem.

What You’ll Learn

  • Homoiconicity and the code-as-data philosophy
  • REPL-driven interactive development
  • Immutable data structures (lists, vectors, maps, sets)
  • Concurrency with atoms, refs, agents, and core.async
  • Interoperability with Java libraries

Why It Matters

Clojure brings the power of Lisp to the JVM — you get the flexibility of a dynamic, interactive language with access to every Java library ever written. Durga Antivirus Pro uses Clojure for its rule-based threat analysis engine where immutable data guarantees thread safety without locks. Companies like Walmart, Netflix, and Apple use Clojure for high-concurrency systems. The REPL workflow means you can develop live against a running system — changing code without restarting — which is transformative for productivity.

Learning Path

    flowchart LR
  A[Clojure Syntax & REPL<br/>You are here] --> B[Immutable Data Structures]
  B --> C[Functional Programming]
  C --> D[Concurrency & State]
  D --> E[Java Interop & Real Apps]
  

Your First Clojure Program

;; hello.clj
(defn greet [name]
  (str "Hello, " name "!"))

(println (greet "Clojure"))

;; Run with: clojure -M hello.clj
clojure -M hello.clj
# Hello, Clojure!

Code Syntax and Homoiconicity

In Clojure, code is data — programs are written as Clojure data structures.

;; Lists (function calls): (operator operand operand...)
(+ 1 2 3)           ;; => 6
(str "Hello" " " "World")  ;; => "Hello World"

;; Vectors (indexed, like arrays)
[1 2 3 4 5]

;; Maps (key-value pairs)
{:name "Alice" :age 30 :language "Clojure"}

;; Sets
#{:a :b :c}

;; Keywords (self-evaluating, used as keys)
:name :age

;; Homoiconicity demonstration
(def my-code '(+ 1 2 3))
(eval my-code)  ;; => 6

;; We can manipulate code as data
(defn double-args [code]
  (map (fn [x] (if (number? x) (* 2 x) x)) code))

(double-args '(+ 1 2 3))  ;; => (+ 2 4 6)
(eval (double-args '(+ 1 2 3)))  ;; => 12
6
(+ 2 4 6)
12

Immutable Data Structures

All Clojure data structures are immutable and persistent — they share structure for efficiency.

;; Vectors
(def v [1 2 3 4 5])
(conj v 6)           ;; => [1 2 3 4 5 6] (original v unchanged)
(get v 2)            ;; => 3
(assoc v 0 100)      ;; => [100 2 3 4 5]
(subvec v 1 3)       ;; => [2 3]

;; Maps
(def m {:name "Bob" :age 25 :city "Berlin"})
(assoc m :job "Engineer")    ;; => {:name "Bob", :age 25, :city "Berlin", :job "Engineer"}
(dissoc m :city)             ;; => {:name "Bob", :age 25}
(update m :age inc)          ;; => {:name "Bob", :age 26, :city "Berlin"}
(merge m {:age 26 :country "DE"})  ;; => {:name "Bob", :age 26, :city "Berlin", :country "DE"}

;; Lists (linked lists, efficient for head access)
(def lst '(1 2 3 4 5))
(peek lst)           ;; => 1
(pop lst)            ;; => (2 3 4 5)
(conj lst 0)         ;; => (0 1 2 3 4 5)

Functional Programming

;; Higher-order functions
(map inc [1 2 3 4 5])             ;; => (2 3 4 5 6)
(filter even? (range 10))          ;; => (0 2 4 6 8)
(reduce + [1 2 3 4 5])            ;; => 15

;; Threading macros (pipeline data through functions)
(->> (range 100)
     (filter odd?)
     (map #(* % %))
     (take 5))
;; => (1 9 25 49 81)

;; Partial application
(def add5 (partial + 5))
(add5 10)  ;; => 15

;; Composing functions
(def process (comp str inc (partial * 2)))
(process 5)  ;; => "11"  (str (inc (* 2 5)))

;; Destructuring
(defn print-person [{:keys [name age city]}]
  (println (str name " is " age " years old from " city)))

(print-person {:name "Alice" :age 30 :city "Paris"})
;; Alice is 30 years old from Paris
Alice is 30 years old from Paris

Concurrency — Atoms, Refs, Agents

Clojure provides separate concurrency primitives for different use cases.

;; Atoms — synchronous, uncoordinated state
(def counter (atom 0))

(swap! counter inc)          ;; atomically increment
(swap! counter + 5)          ;; atomically add 5
(reset! counter 0)           ;; reset to 0

(println @counter)  ;; 0

;; Refs — coordinated, transactional state
(def account-a (ref 1000))
(def account-b (ref 500))

(defn transfer [from to amount]
  (dosync
    (alter from - amount)
    (alter to + amount)))

(transfer account-a account-b 200)
(println @account-a) ;; 800
(println @account-b) ;; 700

;; Agents — asynchronous, independent state
(def log-agent (agent []))

(send log-agent conj "Started")
(send log-agent conj "Processing...")
(send log-agent conj "Completed")

(await log-agent)  ;; wait for all sends to complete
(println @log-agent)
0
800
700
["Started" "Processing..." "Completed"]

Java Interop

Clojure can call any Java code directly.

;; Import Java classes
(import 'java.util.Date
        'java.text.SimpleDateFormat
        'javax.swing.JFrame
        'javax.swing.JLabel
        'java.awt.Font)

;; Call Java methods
(.toUpperCase "hello")                   ;; => "HELLO"
(.startsWith "Clojure" "Clo")            ;; => true
(Math/sqrt 144)                           ;; => 12.0

;; Create Java objects
(def date (Date.))
(println date)  ;; current date

;; Use Java collections
(def list (java.util.ArrayList.))
(.add list "item1")
(.add list "item2")
(println (.size list))  ;; 2

;; Create a Swing window
(defn show-window []
  (doto (JFrame. "Hello from Clojure")
    (.setSize 400 300)
    (.setDefaultCloseOperation JFrame/EXIT_ON_CLOSE)
    (.add (doto (JLabel. "Hello, Clojure Swing!" {})
            (.setHorizontalAlignment JLabel/CENTER)
            (.setFont (Font. "Serif" Font/BOLD 24))))
    (.setVisible true)))

;; (show-window)  ;; uncomment to run
HELLO
true
12.0
2

REPL-Driven Development

;; In a REPL session:
;;
;; user=> (+ 1 2 3)
;; 6
;;
;; user=> (defn square [x] (* x x))
;; #'user/square
;;
;; user=> (square 5)
;; 25
;;
;; user=> (doc map)
;; clojure.core/map
;; ([f coll] [f c1 c2 ...])
;; Returns a lazy sequence...
;;
;; user=> (require '[clojure.string :as str])
;; nil
;;
;; user=> (str/join ", " [1 2 3])
;; "1, 2, 3"

Common Mistakes

1. Forgetting parentheses around function calls

println "hello" throws an error. In Clojure, everything is (function arg arg) — the parentheses are not optional.

2. Using = for assignment instead of comparison

(= x 5) checks equality. Clojure has no variable reassignment in the traditional sense — use def or let for bindings.

3. Mutating state without concurrency primitives

(def m {:a 1}) then (assoc m :a 2) creates a new map — m still points to {:a 1}. Use atoms for mutable references.

4. Excessive nesting (the “Lisp parens” problem)

Deeply nested code is hard to read. Use threading macros (->, ->>, as->) to linearize pipelines.

5. Assuming vectors and lists are interchangeable

Vectors are indexed (O(1) access). Lists are sequential (O(n) access). conj adds to the end of vectors but the front of lists.

6. Forgetting Java interop requires proper imports

Java classes need (import 'package.ClassName) or :import in ns. Using unimported classes causes class-not-found errors.

Practice Questions

  1. What is homoiconicity? Code is represented as data structures of the language itself. Clojure code is written as lists of symbols, which can be manipulated and transformed like any other data.

  2. How do atoms differ from refs? Atoms are for uncoordinated, synchronous state changes. Refs are for coordinated, transactional changes across multiple state points. Both ensure thread safety without locks.

  3. What is the threading macro ->>? Thread-last macro: takes the result of each expression and inserts it as the LAST argument of the next expression. Flattens deeply nested function calls into readable pipelines.

  4. How does Clojure handle immutability efficiently? Persistent data structures share structure across versions. When you “modify” a vector, most of the old vector is reused. This gives O(log n) operations instead of O(n).

  5. What is REPL-driven development? Developing against a running process: you connect a REPL to the application, evaluate code changes on the fly, and see results immediately — without restarting. This enables interactive exploration and rapid iteration.

Challenge: Write a Clojure program that reads a log file, parses lines matching a regex pattern, counts occurrences grouped by severity level (INFO, WARN, ERROR), and prints a summary sorted by count.

Mini Project — Simple HTTP Server

(ns web-server
  (:import (java.net ServerSocket InetAddress)
           (java.io BufferedReader InputStreamReader PrintWriter)))

(defn handle-client [client-socket]
  (try
    (let [input  (BufferedReader. (InputStreamReader. (.getInputStream client-socket)))
          output (PrintWriter. (.getOutputStream client-socket) true)
          request (.readLine input)]
      (println "Request:" request)

      ;; Read headers
      (loop [line (.readLine input)]
        (when (and line (not (.isEmpty line)))
          (println "Header:" line)
          (recur (.readLine input))))

      ;; Send HTTP response
      (.println output "HTTP/1.1 200 OK")
      (.println output "Content-Type: text/html")
      (.println output "")
      (.println output "<!DOCTYPE html>")
      (.println output "<html><body>")
      (.println output "<h1>Clojure HTTP Server</h1>")
      (.println output (str "<p>Requested: " request "</p>"))
      (.println output "<ul>")
      (dotimes [i 5]
        (.println output (str "<li>Item " i "</li>")))
      (.println output "</ul></body></html>"))
    (finally
      (.close client-socket))))

(defn start-server [port]
  (let [server (ServerSocket. port)]
    (println (str "Server running on http://localhost:" port))
    (println "Press Ctrl+C to stop")
    (try
      (while true
        (let [client (.accept server)]
          (future (handle-client client))))
      (finally
        (.close server)))))

;; Start the server
(comment
  (start-server 8080)

  ;; Visit http://localhost:8080 in your browser
  ;; Each request spawns a future (thread) to handle it
  )

FAQ

Is Clojure the same as Common Lisp?
No. Clojure is a modern Lisp dialect on the JVM/CLR/JS. Common Lisp is a separate Lisp standard with CLOS (Common Lisp Object System), a different ecosystem, and no JVM integration. Clojure emphasizes immutability and functional programming more strongly.
Can I use Java libraries from Clojure?
Yes, directly and without wrappers. This is Clojure’s superpower — any Java library (Apache, Spring, log4j, etc.) is available immediately. See the Java Interop section above.
Is Clojure good for web development?
Yes. Ring (HTTP abstraction), Compojure (routing), and Luminus (full framework) make web development productive. The immutable data model matches HTTP’s stateless nature naturally.
How does Clojure compare to Java?
Clojure is more concise (typically 50-70% less code), functional by default, and interactive (REPL-driven). Java has more tooling maturity and a larger developer pool. They interoperate seamlessly — you can mix both in the same project.
Does Clojure have types?
Clojure is dynamically typed. For optional static typing, core.typed and Spec provide type checking and runtime validation respectively. Most Clojure projects use Spec for specification-based testing and validation.
What is core.async?
A library for asynchronous programming using channels and go blocks, inspired by Go’s concurrency model. It allows CSP-style concurrency where goroutines communicate over channels rather than sharing memory.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro