Write readable Clojure with idiomatic indentation, kebab-case names, predicate and mutation markers, namespace-aware formatting, and formatter habits that reduce code review noise.
Clojure style is not about making Lisp look like Java. It is about making nested forms, immutable values, and namespace-qualified calls easy to scan.
For Java engineers, the biggest adjustment is that formatting carries structure. Braces and type declarations are gone, so indentation, naming, and form shape do more work.
| Goal | Java habit to reconsider | Clojure habit to build |
|---|---|---|
| Scan form structure | Align around braces and semicolons | Indent forms by semantic shape |
| Read names quickly | Use camelCase | Use kebab-case symbols |
| See predicates | Prefix with is or has |
End names with ? |
| Mark side effects | Hide mutation inside methods | Use names and boundaries that expose effects |
| Reduce review noise | Debate formatting manually | Let a formatter handle whitespace |
Style is part of correctness because poorly formatted Clojure hides evaluation order.
Function bodies usually indent two spaces:
1(defn invoice-total [invoice]
2 (+ (:subtotal invoice)
3 (:tax invoice)
4 (- (:discount invoice 0))))
Binding forms align name/value pairs:
1(let [subtotal (:subtotal invoice)
2 tax (:tax invoice)
3 discount (:discount invoice 0)]
4 (- (+ subtotal tax) discount))
Threaded pipelines indent each step:
1(-> invoice
2 normalize-lines
3 apply-discounts
4 calculate-tax)
Do not fight the formatter. If indentation looks strange, first ask whether the form shape is too complex.
Clojure names are usually lowercase and hyphenated:
1(defn normalize-email [email]
2 ...)
Important naming markers:
| Marker | Meaning | Example |
|---|---|---|
? |
Predicate returning truthy/falsey value | active-user? |
! |
Side-effecting or state-changing operation | save-user! |
-> |
Conversion | order->invoice |
*earmuffs* |
Dynamic Var | *print-length* |
Leading _ |
Intentionally unused binding | _request |
These markers are compact contracts. They help reviewers notice when code is pure, effectful, conditional, or conversion-oriented.
| Java-style name | Idiomatic Clojure |
|---|---|
getUserById |
user-by-id |
isActive |
active? |
calculateInvoiceTotal |
invoice-total |
saveUser |
save-user! when it performs an effect |
orderToInvoice |
order->invoice |
Use verbs when the operation matters, but do not force every function into a Java method-name pattern. In Clojure, many functions are best named after the value they return.
Readable data literals are as important as readable function calls:
1{:user/id 42
2 :user/email "dev@example.com"
3 :user/roles #{:admin :billing}
4 :user/status :active}
Use alignment when it helps compare related keys. Do not over-align unrelated data until small changes create noisy diffs.
Commas are whitespace in Clojure:
1[:a, :b, :c]
2;; same as
3[:a :b :c]
Most idiomatic code omits commas except when they materially improve readability in dense literals.
Use blank lines to separate ideas, not every expression:
1(defn billable-lines [invoice]
2 (->> (:lines invoice)
3 (filter billable?)
4 (map apply-discount)))
5
6(defn invoice-total [invoice]
7 (->> invoice
8 billable-lines
9 (map :line/total-cents)
10 (reduce + 0)))
Keep comments for intent and tradeoffs. If a comment only explains parentheses, improve the code shape instead.
Use a formatter such as cljfmt through the build tool your project already uses. The exact command depends on the project, but the review rule is stable:
| Workflow point | Action |
|---|---|
| Before committing | Run the formatter or formatting check |
| During review | Discuss semantics, not whitespace |
| When formatter output is ugly | Simplify the form or add a formatter hint only if the team accepts it |
| In mixed Java/Clojure repos | Let each language’s formatter own its files |
Avoid pinning old dependency snippets in learning material unless you are showing a real project file. Tool versions change; the habit matters more here than a stale version number.
Bad formatting often signals too much work in one form:
1(defn total-active-cents [users]
2 (reduce + (map :balance-cents (filter (fn [user] (= :active (:status user))) users))))
Prefer a pipeline with named steps when that helps the reader:
1(defn total-active-cents [users]
2 (->> users
3 (filter active-user?)
4 (map :balance-cents)
5 (reduce + 0)))
This is not merely prettier. It makes the data flow reviewable.
getUserById, isPaid, and saveInvoice into idiomatic Clojure names.let so bindings are easy to scan.map/filter/reduce expression into a ->> pipeline.? or !.? for predicates, ! for effectful operations, and -> for conversions.