Practical signals that a Clojure codebase is drifting away from clarity, local reasoning, and idiomatic design.
A code smell is not a bug by itself. It is a sign that the design may be getting harder to understand, change, or trust. In Clojure, code smells often show up when code drifts away from the language’s strengths: plain data, local reasoning, small composable functions, explicit state boundaries, and honest use of abstraction.
Smells matter because they tell you where refactoring is likely to pay off before the code becomes an emergency.
Some functions have respectable names but conceal too much work:
all at once.
Example:
1(defn process-order! [order]
2 ...)
If process-order! validates, enriches, persists, emits metrics, and sends messages, it is probably too much to understand as one unit. The smell is not only size. It is responsibility density.
When namespaces are named core, util, helpers, or common, the code may still function, but readers lose architectural cues.
Smells include:
core.clj filesThis often means the code’s real responsibilities have never been split clearly enough.
Some branching is normal. It becomes a smell when:
cond trees grow everywhere for the same conceptSometimes the right fix is a helper function. Sometimes it is a protocol, multimethod, or data-driven dispatch table. The smell is duplicated or sprawling decision logic without a clear home.
Code that relies on global vars, large shared atoms, or ambient context can look neat at the call site while becoming difficult to reason about.
Signals:
This smell often indicates a boundary problem rather than just a single bad function.
Clojure offers protocols, multimethods, macros, higher-order functions, and data-driven patterns. They are useful. They also make premature architecture easy.
A smell appears when:
The code becomes harder to read even though the behavior did not become more expressive.
When code is filled with:
nil checks at every boundaryit often means the system lacks strong contracts at the edges.
This smell usually points back to missing validation, unclear function boundaries, or inconsistent data modeling.
Sequence-heavy code is idiomatic until it stops matching the workload.
Smells include:
mapThese are not just performance concerns. They often signal that the data flow is no longer explicit enough.
Names like:
handle-dataprocess-itemdo-workutil-fnmake it harder to see business intent. Clojure often gains readability from short code, but that brevity only works when names still carry responsibility clearly.
Weak names force readers to inspect implementation just to know what the code is for.
flowchart TD
A["Notice friction: hard to read, test, or change"] --> B["Name the smell"]
B --> C["Ask what boundary or responsibility is unclear"]
C --> D["Refactor the smallest useful unit"]
D --> E["Re-check readability and change cost"]
This is useful because smells are diagnostic tools, not moral judgments. The point is not to label code as bad. The point is to understand what kind of improvement would help most.