- Created: 2 Oct 2026
- Last updated: 11 Oct 2026
- Changelog:
- 11 Oct 2026 - Rewrote the contents as HTML instead of plain text file
Feel free to copy, modify and share this document. Please do not remove this attribution.
For now, the items are in no particular order. I'll divide them into sections, revise, and add examples later when it's fun to do so. For now I wanted to capture personal learnings after years of staring at and writing Clojure code. Probably not relevant in the age of AI (until token and memory prices increase...) But I needed to get it out of my system.
Hope you find it useful.
- Avoid function literals, that is, #(). Prefer (fn ...) as it can be named and, most importantly, does not use positional arguments.
- When defining inline lambdas, name them, especially in longer pipelines: (fn assign-value [x] ...). Names appear in stacktraces and will make debugging easier.
- Never publish code looking as if it was pasted from the REPL. Don't write cryptic code. Example of what not do to:
(let [data {:John 33 :Anne 32}] (->> {:x [1 :John] :y [3 :Anne]} (map #(vals (apply select-keys data (vals %&)))) ...)) - Single-arity functions without a docstring or metadata should have their arguments on the same line as the function name.
- Never add blank lines before or after arglists, or use them to visually group sections of a function. It hides the fact that the code wasn't split into functions properly, and once more code is added to that function will need to be split into functions anyway.
- There is never a good reason to use the `as->` macro.
- Most often, a macro name should be prefixed with `with-`. If there is also a function counterpart, it should be prefixed with `wrap-`. When writing simple macros that do some setup and/or teardown and call a function in the middle, the macro can just call the `wrap-` function instead. For example:
(defn wrap-connection [f] (try ... (f) (finally ...))) (defmacro with-connection [& body] `(wrap-connection (fn [] ~@body))) - Do not validate parts of third-party data in different functions. If possible, validate it at the boundary / entry point.
- Do not validate app configuration in different places. Validate it upfront—if it passes, then it should just work.
- Do not use assertions (assert, :pre, and :post conditions) to validate third-party data and app configuration. Use them only if you are really unsure whether something YOU DID might blow up and you prefer the app to crash in a test environment rather than fail silently. Turn off assertions in production.
- Clojure is not a typed language, and if you really need types, switch to another language instead of trying to write OCaml in Clojure.
- If you have a really big project, add schemas for main functions (for example, use Malli)—check inputs and outputs of main entry points. But this type of validation SHOULD NOT be enabled in production.
- Never name your arguments "data" or "input". Everything is data or input.
- Using with-redefs or alter-var-root! in unit tests is wrong and is a code smell... unless it is too late and there is no other way to mock something.
- Prefer testing whole features instead of individual functions (unless, of course, you are implementing general utils — then this doesn't apply).
- When implementing a feature, and where (and if) it makes sense, make the entry point accept a "setup" argument—a hash map whose defaults are references to functions interacting with the outside world, returning current time, accessing caches, etc. This makes features much easier to test.
- When you need to return two or more values from a function, do not return sequences or vectors. With vectors, destructuring is index-based, for example: (let [[status result] ...] ...). It is only a matter of time before more values are needed, and you will eventually have to update every occurrence where it is destructured or used. Use maps; maps are idiomatic in Clojure.
- Never use that more complex destructuring syntax. Never do:
(let [{{person-age :age} 0} ...] ...)Don't even attempt to rename; please, no:(let [{person-age :age} ...] ...) - If a function operates on multiple objects of the same shape, prefer to access or pass around that data without destructuring it or binding it in let blocks. Otherwise, information about the source is lost and it becomes confusing where a particular value comes from. Let's say you have a function that accepts user-1 and user-2. It is better to call (:first-name user-1) three times in that function than to do (let [first-name (:first-name user-1)] ...). (let [user-1-first-name (:first-name user-1)] ...) is ok, though.