Why Clojure Needs Specific AI Coding Rules
Clojure is radically different from mainstream languages. It's a Lisp on the JVM with immutable data structures, a REPL-driven development workflow, and a philosophy that data should be represented as plain maps and vectors — not objects with methods. AI assistants trained on Java, Python, and JavaScript generate Clojure that violates all of these principles.
The most common AI failures in Clojure: creating defrecord/deftype where plain maps would suffice, ignoring threading macros in favor of deeply nested function calls, using atoms and refs excessively instead of pure data flow, generating Java-interop-heavy code instead of idiomatic Clojure, and structuring code as classes rather than namespaces with functions.
Clojure has a small but opinionated community. 'It's just data' is the mantra — and your CLAUDE.md needs to enforce it.
Rule 1: Data-Oriented Design with Plain Maps
The rule: 'Represent domain data as plain Clojure maps with keyword keys — not defrecord or deftype unless you need protocol implementation or Java interop. Use destructuring to access map fields. Use assoc, dissoc, update, and merge for map transformations. Data is always immutable — never mutate in place.'
For namespaced keywords: 'Use namespaced keywords for domain data: {:user/name "Alice" :user/email "alice@example.com"}. Namespaces prevent key collisions when merging maps from different domains. Use the :: shorthand for the current namespace.'
This is the fundamental Clojure rule. AI wants to create types and classes; Clojure wants plain data. 'It's just a map' solves most modeling problems more simply than the AI's instinct to create a class hierarchy.
AI wants to create types and classes. Clojure wants plain maps. {:user/name "Alice" :user/age 30} solves most modeling problems more simply than any class hierarchy.
Rule 2: Threading Macros for Readability
The rule: 'Use threading macros (-> for thread-first, ->> for thread-last) for data transformation pipelines. Never nest more than 2 function calls — thread instead. Use -> when the data is the first argument (map operations): (-> user (assoc :active true) (update :visits inc)). Use ->> when the data is the last argument (sequence operations): (->> items (filter active?) (map :name) (sort)).'
For conditional threading: 'Use some-> for nil-safe threading (short-circuits on nil). Use cond-> for conditional steps in a pipeline. Use as-> when the threaded value needs to be in different argument positions across steps.'
Threading macros are Clojure's equivalent of the pipe operator. AI code without them is immediately recognizable as non-idiomatic — deeply nested parentheses that read inside-out instead of top-to-bottom.
- -> thread-first for map operations (assoc, update, merge)
- ->> thread-last for sequence operations (filter, map, reduce)
- some-> for nil-safe threading — short-circuits on nil
- cond-> for conditional pipeline steps
- Never nest more than 2 calls — thread instead
Deeply nested parentheses that read inside-out are the hallmark of non-idiomatic Clojure. Thread with -> and ->> — code reads top-to-bottom like a data pipeline.
Rule 3: REPL-Driven Development Patterns
The rule: 'Design functions for REPL interaction. Every function should be callable independently with sample data — no hidden dependencies on global state. Use comment blocks (comment ...) for REPL examples that document usage. Use rich comment forms at the bottom of namespaces for development helpers.'
For state management: 'Use atoms for application state that changes (database connections, caches, configuration). Access atoms only at the edges — pass data to pure functions, never pass the atom itself. Use the component or integrant library for system lifecycle management.'
The REPL is central to Clojure development. Functions that can't be called in isolation at the REPL are poorly designed. This rule ensures AI generates REPL-friendly code — small, pure, independently testable functions.
Every function should be callable at the REPL with sample data. If it can't be tested in isolation, it's poorly designed. Use (comment ...) blocks for REPL examples.
Rule 4: Validation with spec or malli
The rule: 'Use malli (or clojure.spec) for data validation at system boundaries — API inputs, database results, configuration. Define schemas as data (malli's strength). Validate at entry points, not throughout business logic — trust the data once validated. Use coercion for transforming external data into internal representations.'
For malli: 'Define schemas as Clojure data structures: [:map [:name :string] [:age [:int {:min 0}]]]. Use m/validate for boolean checks, m/explain for error details, m/coerce for transformation. Attach schemas to function signatures for documentation and generative testing.'
For clojure.spec: 'Use s/def for named specs, s/keys for map specs, s/fdef for function specs. Use spec instrumentation in development to catch violations. Use spec generators for property-based testing with test.check.'
Rule 5: Ring, Reitit, and Web Conventions
The rule: 'Use Ring for HTTP handling — request and response are plain maps. Use Reitit for routing — data-driven route definitions. Middleware is function composition — keep the middleware stack explicit and ordered. Use ring-json for JSON serialization. Return proper HTTP status maps: {:status 200 :body data :headers {"Content-Type" "application/json"}}.'
For API design: 'Handlers are plain functions that take a request map and return a response map. Keep handlers thin — delegate to domain functions. Middleware handles cross-cutting concerns (auth, logging, CORS). Never put business logic in middleware.'
For database access: 'Use next.jdbc for JDBC database access. Use HugSQL or HoneySQL for SQL query management. Represent database results as maps — never create ORM-style objects. Keep database functions in a separate namespace from business logic.'
Complete Clojure Rules Template
Consolidated template for Clojure teams.
- Plain maps for domain data — defrecord only for protocols/Java interop
- Threading macros (-> ->>) for all pipelines — never nest more than 2 calls
- REPL-friendly functions — independently callable, comment blocks for examples
- Atoms at the edges only — pass data to pure functions, not atoms
- malli/spec for validation at boundaries — trust data after validation
- Ring request/response maps — Reitit for routing — thin handlers
- next.jdbc for database — results as maps, not ORM objects
- cljfmt for formatting — clj-kondo for linting — both in CI