Best Practices for Production Clojure
Prerequisites: Testing with clojure.test, clojure.spec
Clojure rewards a particular way of structuring programs. These habits are common to well-run Clojure code bases.
Functional core, imperative shell
Keep business logic in pure functions over plain data, and push side effects (database, HTTP, time, randomness) to a thin outer layer. Pure functions are trivial to test and to try at the REPL.
;; Pure core: decisions as data
(defn plan-restock [inventory {:keys [threshold order-qty]}]
(for [[item qty] (sort inventory)
:when (< qty threshold)]
{:action :order :item item :qty order-qty}))
(defn apply-deliveries [inventory deliveries]
(reduce (fn [inv {:keys [item qty]}] (update inv item (fnil + 0) qty))
inventory
deliveries))
;; Imperative shell: the only place with side effects
(defn execute-plan! [state-atom settings]
(let [plan (plan-restock @state-atom settings)]
(doseq [{:keys [item qty]} plan]
(println "ordering" qty "x" (name item))) ; would call an API
(swap! state-atom apply-deliveries plan)))
(def stock (atom {:apples 2 :bread 12 :milk 1}))
(execute-plan! stock {:threshold 5 :order-qty 10})
(println @stock)
ordering 10 x apples
ordering 10 x milk
{:apples 12, :bread 12, :milk 11}
plan-restock returns what should happen as data; only
execute-plan! makes it happen. Testing the core needs no mocks — just
call it with a map and compare the result.
Data design
- Prefer maps with namespaced keywords (
:order/id) for domain data; they're self-describing and don't collide. - Validate data at system boundaries (HTTP input, config, messages) with spec or Malli — not everywhere.
- Keep data flat enough to be easy to navigate; use
get-in/update-inpaths when it isn't.
State and concurrency
- A few atoms holding plain data; update them with pure functions.
- Keep side effects out of
swap!functions anddosyncblocks (they may retry). - Manage stateful resources (servers, pools) with a lifecycle library,
not global
defs that connect at load time.
Code
- Small functions with docstrings; namespaces organized by domain.
- Threading macros over deep nesting; destructuring over repeated
(:key m). - Functions before macros; macros only for syntax.
- Turn on
*warn-on-reflection*; run clj-kondo in CI.
Security
- Read external data with
clojure.edn/read-string, neverclojure.core/read-stringoreval. - Parameterized SQL (
["... WHERE id = ?" id]), never string concatenation. - Secrets in environment variables; production REPLs bound to localhost and access-controlled.
- Keep dependencies updated (
clj -X:deps tree,antqto find outdated ones).
What's next
Try the Advanced Projects, then read What You Should Know Now — and What's Next for where to go from here.