Writing Idiomatic Clojure
Idiomatic Clojure favors small, pure functions, data-oriented design using plain maps and vectors over custom types, and Rich Hickey's "just use a hash-map" philosophy from talks like Simple Made Easy. Following community conventions from clojure.org and widely used libraries matters more in Clojure than in many languages, because its dynamic nature makes consistency of style — not a strict compiler — the main safeguard against confusing code.
Cricket analogy: Idiomatic Clojure is like batting in the textbook MCC coaching manual style that any teammate immediately recognizes, rather than an unorthodox method that works but confuses the rest of the dressing room during a run chase.
Function Design and Namespace Organization
Keep functions small and single-purpose, name predicates with a trailing ? (empty?, valid-order?), name side-effecting or mutating functions with a trailing ! (reset!, swap!), and prefer kebab-case for all symbols, such as parse-config rather than parseConfig. Organize code into namespaces that each cover one responsibility, requiring dependencies explicitly at the top of the file with :require and aliasing them with :as so call sites like (str/join ", " xs) stay readable at a glance.
Cricket analogy: Naming a predicate out? is like an umpire's clear signal, one recognizable gesture per ruling, so every fielder instantly understands the call, while swap!'s trailing ! flags a state-changing action the way a DRS review request flags a decision in progress.
(ns myapp.orders
(:require [clojure.string :as str]
[myapp.pricing :as pricing]))
(defn valid-order?
"Returns true if the order map has all required keys and a positive total."
[{:keys [items total] :as order}]
(and (seq items) (pos? total)))
(defn apply-discount!
"Applies the discount to the running total atom (has side effects, hence the !)."
[totals-atom order-id discount-pct]
{:pre [(<= 0 discount-pct 100)]}
(swap! totals-atom update order-id
#(pricing/discount % discount-pct)))Immutability, State, and Side Effects
Default to pure functions with no side effects, and when state is unavoidable, choose the narrowest concurrency primitive that fits: an atom for independent, synchronous, uncoordinated state; a ref for coordinated synchronous updates across multiple pieces of state via dosync; and an agent for asynchronous, fire-and-forget updates. Avoid def for anything other than top-level constants or functions — using def inside a function to fake mutable local state is a well-known anti-pattern that breaks referential transparency.
Cricket analogy: Choosing an atom for a single independent counter is like tracking one bowler's over-count alone, while a ref-based dosync transaction is like updating both team totals and required-run-rate together atomically after every ball, since they must always stay consistent.
Prefer swap! with a pure update function over reset! with a precomputed value, because swap! is safe under contention: it automatically retries if another thread changed the atom first, whereas reset! with a value computed from a stale read can silently discard a concurrent update.
Error Handling and Testing
Use ex-info to attach structured data to exceptions, such as (ex-info "Order not found" {:order-id id :type ::not-found}), instead of throwing bare strings, so callers can catch the exception and inspect its data with ex-data. Write tests with clojure.test using deftest and is, keep them colocated in a mirrored test/ source tree, and reach for property-based tests via test.check for functions with algebraic invariants, like sorting or parsing round-trips.
Cricket analogy: Using ex-info to attach {:type ::not-found :order-id id} is like a third umpire's decision arriving with full data — ball-tracking, snicko — attached, not just "OUT," so the review team can inspect exactly why rather than a bare shout with no evidence.
Avoid catching Throwable or Exception broadly and swallowing it silently. In Clojure this can mask a StackOverflowError from deep non-tail recursion, or hide an agent-thread error that would otherwise surface later via agent-error, making bugs far harder to diagnose.
Threading Macros and Readability
Use the threading macros -> and ->> to express a pipeline of transformations left to right instead of nesting calls inside-out, which mirrors how you'd narrate the steps aloud and is far easier to review. Reach for as-> when a value needs to shift between being the first and last argument across steps, and some-> or some->> when any step in the pipeline might return nil and you want to short-circuit instead of raising a NullPointerException.
Cricket analogy: The -> threading macro reads like ball-by-ball commentary narrating each step left to right, bowled, edged, caught, while deeply nested function calls are like reconstructing the same dismissal backward from a bare scorecard entry.
- Favor small, pure, single-purpose functions named with kebab-case; use trailing ? for predicates and trailing ! for functions with side effects.
- Organize code into single-responsibility namespaces, requiring dependencies explicitly and aliasing them with :as.
- Choose the narrowest state primitive that fits: atom for independent state, ref/dosync for coordinated state, agent for async fire-and-forget.
- Never put side effects inside a swap! update function, since it may retry multiple times under contention.
- Use ex-info with structured data instead of bare strings so callers can catch and inspect failures via ex-data.
- Avoid broad, silent Throwable/Exception catches — they can mask stack overflows and agent errors.
- Use -> and ->> to write readable, top-to-bottom transformation pipelines instead of deeply nested calls.
Practice what you learned
1. What does a trailing ! in a Clojure function name like swap! or reset! conventionally signal?
2. Which state primitive should you choose when two or more pieces of state must change together atomically?
3. Why is it a bad idea to put a println or HTTP call inside a function passed to swap!?
4. What is the idiomatic way to raise an exception with structured, inspectable context in Clojure?
5. What is the main benefit of using the -> threading macro over deeply nested function calls?
Was this page helpful?
You May Also Like
Clojure vs Common Lisp
A practical comparison of Clojure and Common Lisp covering data structures, the JVM host, macros, object systems, and concurrency, for programmers deciding between the two Lisps or moving from one to the other.
Building a Web App with Ring
How to build a Clojure web application on Ring: the handler/middleware contract, request and response maps, routing with Reitit or Compojure, and running a Jetty server.
Clojure Quick Reference
A scannable cheat sheet of core Clojure syntax, everyday sequence and map functions, the four state/concurrency primitives, and the project tooling and REPL commands used daily.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics