Clojure-specific philosophy, idioms, and judgment frameworks. Use when working with Clojure code. Emphasizes data-oriented design, runtime validation with spec, REPL-driven development, and the philosophy of simplicity over ease.
Target Clojure 1.12.3+ (current stable as of September 2025). Version history at https://github.com/clojure/clojure/blob/master/changes.md.
This skill provides judgment frameworks and philosophical grounding for expert-level Clojure development. It assumes you understand functional programming and Lisp basics from training, focusing instead on Clojure-specific philosophy, when/why guidance, and retrieval triggers for avoiding common mistakes.
<core_philosophy>
For foundational software engineering principles, see the software-engineer skill. For general FP principles, see the functional-programmer skill—this skill provides Clojure-specific guidance that supersedes those general patterns.
Rich Hickey's foundational distinction: "Simple" means one thing, one concept, one task. "Easy" means familiar, near at hand. Always choose simple over easy.
This philosophy permeates every Clojure decision:
The critical insight: Simplicity enables change. Easy rots into complexity. When facing a choice between "this feels familiar from Java/Python/JavaScript" and "this is the Clojure way," choose the Clojure way. The unfamiliarity is temporary; the simplicity compounds.
"Programs must be written for people to read, and only incidentally for machines to execute." — Hal Abelson, SICP
From Rich Hickey's Spec-ulation: Clojure's design was inspired by RDF's open-world assumption. Code is an ontology of a solution. Names create enduring semantic commitments.
"If I put on a hat, it does not change what my family is." — Rich Hickey
Maps are collections of keys, not the stuff inside the keys. Extra keys don't change identity:
;; These are the same entity with different knowledge
{:user/id 123 :user/name "Alice"}
{:user/id 123 :user/name "Alice" :user/email "alice@example.com"}
;; Identity preserved, knowledge accreted
This is RDF's open-world assumption:
"Spec is about what you CAN do, not about what you CAN'T." — Rich Hickey
Why spec doesn't allow closed maps:
;; If you could say "only these keys"
(s/def ::user (s/closed-keys :req [::id ::name]))
;; Then adding ::email later would BREAK everything
;; Every consumer would need updating
;; Growth becomes breakage
;; Cascading changes up the dependency tree
The open-world discipline:
(select-keys user [::id ::name])Each level is a collection with two operations: add or remove.
"My family doesn't change when I put on a hat." A function changing (putting on a hat) doesn't change the namespace (the family). Don't version the namespace because a function changed. Don't version the artifact because a namespace changed.
Growth happens through accretion:
Breakage happens through:
The discipline: Turn breakage into accretion. Don't remove foo, add foo-2. Don't require new parameters, make them optional. Don't remove keys from returns, add new ones.
For knowledge engineers: This is why Clojure feels natural for ontology work. The language's data model mirrors RDF's assumptions:
Write Clojure code the way you'd design an ontology: Open to extension, explicit about semantics, preserving meaning over time. </core_philosophy>
<repl_driven>
The REPL isn't just a tool—it's how you think about Clojure development. Stuart Halloway: "You're living in your program invoking your tools, instead of living in your tools invoking your program."
Traditional development: Write code → Compile → Run → Debug → Repeat REPL-driven development: Start program → Develop features while running → Test immediately → Never restart
What this means practically:
Standard practice for preserving REPL experiments:
(comment
;; REPL-driven development workflow
(def test-user {:user/id 123 :user/name "Alice"})
(create-user test-user)
(db/query {:user/id 123})
;; These forms don't execute in production
;; But preserve your development process
;; And serve as executable documentation
)
Why this matters: The comment block captures your development thinking. Future you (or other developers) can see how you explored the problem space. This is executable documentation of the design process.
I cannot use IDE-integrated REPLs (CIDER, Calva). Terminal-based REPL invocation hangs the bash tool. I work differently:
This is actually good discipline: Code that works without complex REPL state is more robust, testable, and maintainable. </repl_driven>
<data_oriented>
Rich Hickey's guidance: "Just use maps." This isn't laziness—it's profound design insight.
Separate code from data:
Why maps are the default:
;; Good: namespaced keywords
{:user/id 123
:user/name "Alice"
:user/email "alice@example.com"
:account/id 456
:account/balance 1000.0
:account/currency :USD}
;; Bad: unqualified keys risk collisions
{:id 123 ; Which id? User or account?
:name "Alice"
:balance 1000.0}
Auto-resolved keywords in the namespace:
(ns myapp.users)
;; ::name resolves to :myapp.users/name
{::id 123
::name "Alice"
::email "alice@example.com"}
Maps should be 95% of your data structures. Records exist for specific cases:
Use records when:
Use maps when:
Don't create records prematurely. Start with maps. Only extract records when you have evidence they're needed. The flexibility of maps compounds over time.
EDN (Extensible Data Notation): Subset of Clojure syntax for data serialization.
Use EDN for:
Use JSON when:
Critical positioning: Spec provides runtime validation with detailed error messages. It is NOT compile-time type checking. The namespace clojure.spec.alpha is production-ready despite "alpha" (indicates potential API evolution, not quality).
At system boundaries:
Where spec shines:
Don't over-specify:
The judgment call: Spec at boundaries where bad data enters. Don't spec internal implementation details. Trust your code within the boundary.
Register specs with namespaced keywords:
(require '[clojure.spec.alpha :as s])
;; Predicate-based specs
(s/def ::id pos-int?)
(s/def ::email (s/and string? #(re-matches #".+@.+\..+" %)))
(s/def ::role #{:admin :user :guest})
;; Map specs - note: maps are OPEN by default
(s/def ::user
(s/keys :req [::id ::email]
:opt [::role ::created-at]))
Co-locate specs with code:
(ns myapp.users
(:require [clojure.spec.alpha :as s]))
(s/def ::id pos-int?)
(s/def ::email string?)
(s/def ::user (s/keys :req [::id ::email]))
(defn create-user
"Creates a new user account."
[user-data]
(let [validated (s/conform ::user user-data)]
(if (s/invalid? validated)
(throw (ex-info "Invalid user data"
(s/explain-data ::user user-data)))
(db/insert! :users validated))))
(s/fdef create-user
:args (s/cat :user-data ::user)
:ret ::user
:fn (s/and #(= (-> % :args :user-data ::email)
(-> % :ret ::email))))
Instrument in development only:
(require '[clojure.spec.test.alpha :as stest])
;; Development/test: validates :args at call time
(stest/instrument `create-user)
;; Production: turn off (performance overhead)
(stest/unstrument `create-user)
Specs automatically generate test data:
(require '[clojure.spec.gen.alpha :as gen])
(gen/sample (s/gen ::user))
;; Generates random valid users
(stest/check `create-user)
;; Property-based testing with generated inputs
Maps are open by default: Extra keys allowed. This is intentional—enables evolution. Use s/keys not s/strict-keys unless you have a specific reason.
The spec IS the function: It defines correctness. Design specs carefully.
Don't fight the alpha status: clojure.spec.alpha is stable and widely used. The "alpha" means "we might make a new namespace for breaking changes" not "this is experimental."
spec2/spec-alpha2: Work in progress with no timeline. Stick with spec.alpha for all production code. </spec>
<state_management>
Clojure provides different reference types for different coordination needs. Understanding when to use each is critical.
Atoms (90% of state):
swap!, reset!, compare-and-set!(def app-state (atom {:users {} :sessions {}}))
(swap! app-state update-in [:users user-id] assoc :last-seen (Instant/now))
Refs (coordinated transactions):
(def account-a (ref 1000))
(def account-b (ref 500))
(dosync
(alter account-a - 100)
(alter account-b + 100))
Agents (asynchronous updates):
send for CPU-bound, send-off for blocking I/O(def background-processor (agent {}))
(send background-processor process-batch data)
Vars with dynamic scope:
*out*, *print-length*, context propagationDon't use atoms for everything just because they're simple:
;; Anti-pattern: global mutable state everywhere
(defonce web-server (atom nil))
(defonce db-connection (atom nil))
(defonce cache (atom {}))
(defn start! []
(reset! web-server (create-server))
(reset! db-connection (connect-db)))
Better: Component/Mount/Integrant for lifecycle:
(defn create-system []
{:web-server (create-server)
:database (connect-db)
:cache {}})
Why: Global atoms create hidden dependencies, make testing hard, and obscure system boundaries. Use lifecycle libraries for managing stateful resources. </state_management>
<common_mistakes>
These patterns are retrieval triggers—seeing them should activate knowledge about the Clojure alternative.
<from_java>
Overusing class hierarchies where maps suffice:
;; Java thinking: types for everything
(defrecord User [id name email])
(defrecord Admin [id name email permissions])
(defrecord Guest [id name])
;; Clojure thinking: just data
{:user/id 123 :user/name "Alice" :user/role :admin :user/permissions #{:read :write}}
{:user/id 456 :user/name "Guest" :user/role :guest}
Creating unnecessary mutable state:
;; Java thinking: everything needs state management
(defonce users (atom {}))
(defn create-user! [user-data]
(let [id (generate-id)
user (assoc user-data :user/id id)]
(swap! users assoc id user)
user))
;; Clojure thinking: pass data through
(defn create-user [db user-data]
(let [user (assoc user-data :user/id (generate-id))]
(db/insert! db :users user)))
The contains? confusion:
;; Java: Collection#contains checks for VALUE
;; Clojure: contains? checks for KEY/INDEX
(contains? {:a 1 :b 2} :a) ; true (key exists)
(contains? {:a 1 :b 2} 1) ; false (1 is not a key)
(contains? [0 1 2] 1) ; true (index 1 exists)
;; For value membership, use:
(some #{1} (vals {:a 1 :b 2})) ; 1 (truthy)
Not enabling reflection warnings:
;; Add to dev/user.clj
(set! *warn-on-reflection* true)
;; Causes compiler warnings for reflection
;; Fix with type hints in hot paths only
Making everything private:
Clojure trusts developers. Use *.impl.* namespaces for internal details rather than making everything defn-.
</from_java>
<from_python>
Expecting eager evaluation:
;; Lazy sequences don't realize until needed
(def results (map expensive-operation data))
;; Nothing has run yet!
;; Force realization when you need it
(doall (map expensive-operation data)) ; Realizes and returns seq
(dorun (map expensive-operation data)) ; Realizes for side effects, returns nil
(into [] (map expensive-operation data)) ; Realizes into vector
Printing infinite sequences hangs:
;; Don't do this
(println (range)) ; Hangs trying to realize infinity
;; Do this
(println (take 10 (range))) ; Finite!
Holding sequence heads causes space leaks:
;; Bad: holds entire sequence in memory
(let [data (range 1000000)]
(println (first data))
(println (last data))) ; Entire sequence retained!
;; Good: don't hold the head
(println (first (range 1000000)))
(println (last (range 1000000))) ; Separate realization
</from_python>
<from_javascript>
Trying to use async/await directly:
;; JavaScript async/await doesn't exist in Clojure
;; Use core.async for coordination (not all async operations!)
(require '[clojure.core.async :as async])
(defn fetch-user [id]
(async/go
(let [response (async/<! (http/get (str "/users/" id)))]
(:body response))))
Over-using core.async:
;; Don't over-complicate
;; Simple callbacks are fine!
;; Overkill
(async/go
(let [result (async/<! (simple-async-op))]
(process result)))
;; Better: just use a callback
(simple-async-op #(process %))
Expecting 'this' binding:
Clojure has no implicit this. Pass data explicitly.
</from_javascript>
<from_haskell>
Creating type-level abstractions with macros:
Don't recreate Haskell's type system. Embrace dynamic runtime validation with spec.
Assuming purity enforcement:
;; Clojure: pragmatic impurity
;; Just do side effects where needed
(defn get-user-name []
(println "Name: ")
(read-line))
;; Isolate at boundaries, but don't fight them
Avoiding necessary runtime validation:
;; Haskell thinking: types ensure correctness
;; Clojure thinking: validate at boundaries
(defn create-user [user-data]
(let [validated (s/conform ::user user-data)]
(when (s/invalid? validated)
(throw (ex-info "Invalid user"
(s/explain-data ::user user-data))))
(db/insert! :users validated)))
</from_haskell>
<from_imperative>
Not using recur for tail recursion:
;; Stack overflow: JVM has no TCO
(defn factorial [n]
(if (zero? n)
1
(* n (factorial (dec n)))))
;; Correct: use recur
(defn factorial
([n] (factorial n 1))
([n acc]
(if (zero? n)
acc
(recur (dec n) (* acc n)))))
Misusing equality:
;; Use = by default for structural equality
(= 1 1.0) ; false (different types)
(== 1 1.0) ; true (numeric equality only)
;; = for collections
(= [1 2 3] [1 2 3]) ; true
</from_imperative> </common_mistakes>
<advanced_topics>
Rich Hickey: "Don't use macros unless you absolutely need to."
Use macros when you need to:
Use functions for everything else. Functions are the default.
Anti-patterns:
;; Don't write a macro for this
(defmacro double [x]
`(* 2 ~x))
;; Write a function
(defn double [x]
(* 2 x))
When you do write macros: Create usage examples first. Break complex macros into helper functions. Prefer syntax-quote forms.
Transducers provide composable, context-independent transformations.
Use transducers when:
Don't use when:
;; Without transducers
(->> data
(map parse)
(filter valid?)
(map transform)) ; Three intermediate seqs
;; With transducers (single pass)
(def xf (comp (map parse)
(filter valid?)
(map transform)))
(into [] xf data)
(transduce xf conj [] data)
core.async serves specific coordination needs, not all async operations.
Use core.async for:
Avoid core.async for:
thread not go)Key patterns:
go for parking (lightweight, many thousands possible)thread for blocking operations<Always measure before optimizing.
Profile first:
High-level optimization (do first):
Type hints (only if reflection is the problem):
(set! *warn-on-reflection* true) in developmentPrimitive optimization (measure first!):
Verify improvements:
Anti-patterns:
The 90/10 rule: 90% of time spent in 10% of code. Find that 10% first. </advanced_topics>
<tooling> ## Mandatory Tooling StackSearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer