This repository catalogs code smells in Clojure, providing descriptions, examples and causes.
Find a file
2026-06-11 11:56:38 -03:00
examples refactor: provides minor adjustments to the example and smell name 2025-07-23 09:43:10 -03:00
index.html Create index.html 2025-10-06 09:42:17 -03:00
LICENSE Add MIT License to the project 2025-10-16 09:26:51 -03:00
README.md Revise non-idiomatic record construction description 2026-06-09 09:32:59 -03:00
sources-and-excerpts.md docs: add Clojure documentation sources and excerpts 2026-06-11 11:56:38 -03:00

Catalog of Clojure-related code smells

Table of Contents

Introduction

Software systems evolve continuously, and maintaining internal code quality is essential for long-term maintainability and reduced maintenance effort. Code smells are commonly used indicators of potential design or implementation issues that may affect readability, evolution, and refactoring. While well established in object-oriented systems, functional programming languages remain comparatively underexplored, leaving open questions about how these quality concerns manifest in practice. Motivated by this gap, we investigate code smells in the Clojure ecosystem using grey literature.

This catalog consolidates 34 Clojure-specific code smells identified through grey literature analysis. The smells are organized into four categories — State & Concurrency, Module Boundaries & Data Contracts, Logic Flow & Readability, and Environment & Idioms — each grouping related smells according to different dimensions of maintainability in Clojure systems.

Each code smell is documented using the following structure:

  • Name: Identifier of the code smell, used to facilitate communication between developers.
  • Description: Explanation of how the smell may harm code quality and impact maintainability.
  • Example: Code snippet illustrating the occurrence of the smell.
  • Sources and Excerpts: Evidence from grey literature, including developer discussions that motivate and support the smell.

Contributions are highly welcome. Readers are encouraged to open issues or submit pull requests for any corrections, improvements, or additions to the catalog. Further details about the methodology can be found in the Methodology section.

↑ Back to table of contents ↑


State & Concurrency

This category focuses on how Clojure systems manage data identity over time, mutable state, and asynchronous execution.

Immutability Violation

  • Description: This code smell occurs when mutable state is used in a language or paradigm that emphasizes immutability (such as Clojure), leading to side effects, reduced predictability, and harder-to-maintain code.

  • Example:

;; Example from source
(def countries (do-get-countries))

(defn update-country [country]
  (def countries (assoc countries (:name country) country)))

(update-country {:name "Brazil" :pop 210})

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Blocking Inside Go

  • Description: This code smell occurs when a blocking operation (e.g., a/alts!!, a/<!!, a/>!!, or other blocking I/O calls) is used inside a go block. go blocks are designed for non-blocking concurrency and execute on a finite thread pool. Introducing blocking calls inside them violates this model, potentially exhausting the thread pool and preventing other tasks from progressing. This can lead to thread starvation, deadlocks, and overall performance degradation.

  • Example:

;; Not from source
(a/go
  (a/alts!! [started-chan (a/timeout 1000)])
  (a/close! result-chan))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Nested Atoms

  • Description: Storing an Atom or other managed reference (like a Volatile or Ref) inside another Atom. This is an anti-pattern because it violates the principle of atomic state management. Updating the inner Atom does not update the outer Atom's value, making it impossible to guarantee a single, consistent snapshot of the overall state at any time. This might lead to complicated state transitions and undermine the simplicity of the state model.

  • Example:

;; Not from source
(def global-state
  (atom {:ui-state  {:theme :light}
         :history (atom [])}))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Misuse of Dynamic Scope

  • Description: Occurs when dynamic variables (def ^:dynamic) and the binding macro are used without a compelling reason, such as for holding core application data or for passing information in asynchronous/multi-threaded contexts. This introduces hidden mutable state, makes the code hard to debug due to implicit dependencies, and should be reserved only for well-understood contextual configuration or explicit Dynamically Scoped Resources.

  • Example:

(def ^:dynamic *restarts* [])

(def ^:dynamic *restart-bindings* {})

(def ^:dynamic *call-stack* [])

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Overengineering with core.async

  • Description: The misuse of the full abstraction of the clojure.core.async library for simple asynchronous tasks, such as returning a result that only involves a single value or a one-time response. Channels are designed for complex streams of events or coordinating multiple concurrent processes. Using channels for simple tasks introduces unnecessary complexity, increases cognitive load, and adds overhead when simpler, more expressive abstractions like Promises/Deferreds (e.g., promesa) or direct callbacks are sufficient and more idiomatic.

  • Example:

;; Not from source
(ns my-app.async-smell
  (:require [clojure.core.async :as a]))

(defn fetch-single-result-smelly [url]
  (let [ch (a/chan)]
    (a/go (a/>! ch (http/get url)))
    ch))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Dynamically-Scoped Singleton Resource

  • Description: The use of Dynamic Variables (def ^:dynamic) and the binding macro to implicitly manage and pass critical, transactional resources (such as database connections, active transactions, or thread pools). This anti-pattern prevents thread dispatch, breaks lazy sequences, limits the application to one resource per thread, and creates External Data Coupling by hiding dependencies in implicit, thread-local state instead of passing them as explicit function arguments. This greatly increases the risk of silent transactional failure and debugging difficulty.

  • Example:

;; Example from source
(ns com.example.library)

(def ^:dynamic *resource*)

(defn- internal-procedure []
  ;; ... uses *resource* ...
  )

(defn public-api-function [arg]
  ;; ... calls internal-procedure ...
  )

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Unnecessary Laziness

  • Description: The default use of lazy sequence functions (e.g., map, filter) when an eager sequence function (e.g., mapv, into []) would be more efficient, less complex, and better communicate the developer's intent. Using lazy sequences without a specific need (like infinite length or controlled side effects) adds complexity, risks unexpected realization bugs, and contributes to the Lazy Side Effects smell by making performance unpredictable.

  • Example:

;; Not from source
(defn process-smelly [coll]
  (let [doubled (map #(* 2 %) coll)]
    (vec doubled))) ; Forces realization later

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Environment & Idioms

This category focuses on how Clojure code interacts with the languages core abstractions, runtime behavior, and idiomatic conventions.

Unmanaged Resource I/O

  • Description: The failure to use the with-open macro when dealing with resources that implement java.io.Closeable (e.g., Reader, Writer, sockets, or streams). This omission leads to resource leaks and potential system instability by preventing the resource from being released back to the operating system. This violates the principle of managed side effects in I/O operations and must be fixed with the explicit use of with-open to guarantee cleanup.

  • Example:

;; Not from source
(defn read-file-smelly [filename]
  (let [reader (io/reader filename)]
    (line-seq reader)))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Relying on Load-Time Side Effects

  • Description: The practice of relying on a function or macro's output being static or globally available because it was evaluated as a top-level form during namespace loading. This is a anti-pattern because the code relies on the unmanaged side effect of the loading process and the mutable state of the running system. This fragility breaks static analysis, causes non-deterministic behavior during REPL reloading, and violates the functional contract that code should be safe regardless of execution order.

  • Example:

;; Not from source
(ns my-app.fragile-config)
;; Relies on this complex function running *only once* at load time
(def CONFIG (calculate-heavy-config (some/global-atom)))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Namespace Load Side Effects

  • Description: Performing operations such as require or requiring-resolve in a top-level form outside of the primary ns macro. This is a anti-pattern because it introduces hidden, dynamic dependencies that bypass the build tool's static dependency graph. This breaks predictable load ordering, leading to non-deterministic compilation, making the code harder to analyze, test, and maintain.

  • Example:

;; Not from source
(ns my-app.core)
(def some-var
  (requiring-resolve 'my-app.config/get-setting))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Multiple Evaluation in Macros

  • Description: This smell occurs when a macro inserts one of its input argument forms (which can be an arbitrary expression) into the generated code more than once without first binding it to a local, temporary variable (e.g., using a gensym like let [value# ~value]). This is a violation of macro hygiene and leads to Hidden Side Effects: the argument expression is unintentionally evaluated multiple times, causing performance degradation or triggering unwanted side effects for the caller.

  • Example:

;; Not from source
(def counter (atom 0))

(defmacro double-log [x]
  `(do (println "Value:" ~x) (swap! counter inc) (println "Value:" ~x)))

(double-log (swap! counter inc)) 

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Direct usage of clojure.lang.RT

  • Description: This code smell occurs when Clojure code directly invokes methods from the clojure.lang.RT class, such as clojure.lang.RT/iter, to perform operations that are not exposed through the public Clojure API. The RT class is part of Clojure's internal implementation and is not intended for direct use in application code. Directly invoking methods from this class can lead to fragile code that is susceptible to breakage with future updates to the language.

  • Example:

(iterator-seq (clojure.lang.RT/iter [1 2 3]))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Unnecessary Macros

  • Description: This code smell occurs when macros are used in situations where simpler, more conventional solutions—such as functions or existing language constructs—would suffice. While macros offer powerful metaprogramming capabilities, their overuse introduces unnecessary abstraction and complexity. This can obscure the codes intent, make debugging more challenging and reducing maintainability.

  • Example:

(defmacro log-and-exec [expr]
  `(do
     (println "Running...")
     ~expr))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Production doall

  • Description: This code smell occurs when the doall function is used in production code to force realization of lazy sequences, often to trigger side effects or avoid deferred evaluation. While doall can be useful in REPL experimentation, its use in production undermines one of Clojures core strengths: laziness. For large or infinite sequences, this can lead to memory spikes and unpredictable performance. In production contexts, doall often signals poor abstraction choices and should prompt reconsideration of the control flow or evaluation strategy.

  • Example:

(defn print-evens []
  (doall (map #(println %) (filter even? (range 1000)))))

(print-evens)

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Misuse of Channel Closing Semantics

  • Description: This smell occurs when a channel-based data stream uses a custom sentinel value (e.g., :done, :EOF, ::end) to signal termination, rather than relying on the standard core.async semantic contract. This contract dictates that a stream is terminated by closing the channel with a/close!, which causes subsequent reads (a/<!) to return nil. Violating this semantic coupling introduces brittle inspection logic ((when (not= :done event) ...)) and breaks the idiomatic flow control pattern of when-let and loop/recur.

  • Example:

;; Not from source
(def my-chan (a/chan 1))
(a/put! my-chan :done)

(a/go
  (loop []
    (when-let [event (a/<! my-chan)]
      (when (not= event :done) ; <-- Brittle sentinel check
        (prn "Processing" event)
        (recur)))))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Refs in Dependency Vector

  • Description: The anti-pattern of placing a mutable state reference object (an Atom, a use-state object, or a raw use-ref object) directly into a hook's dependency array ([]). Dependency arrays rely on comparing values for change detection. Placing the reference object often causes the hook to either never re-run (if the reference is stable) or re-run unexpectedly (if the framework updates the reference). The idiomatic solution is to track the dereferenced value (@ref) instead.

  • Example:

;; Not from source
(def my-atom-ref (r/atom 0))

(defn MyComponent []
  (r/use-effect
    (fn []
      (println "Value is now:" @my-atom-ref))
    [my-atom-ref])) 

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Improper Emptiness Check

  • Description: This code smell occurs when developers use verbose or less idiomatic constructs—such as (not (empty? x))—to determine whether a collection is non-empty, instead of leveraging the more concise and expressive idiom (seq x). In Clojure, the concept of emptiness is nuanced: nil is considered empty, sequences can be infinite or lazy, and realization may matter. Using seq not only simplifies the check but also aligns with Clojures idiomatic style, improving readability and avoiding redundant negation or abstraction layers.

  • Example:

;; Example from source

(when (not (empty? x)) ...)

(when-not (empty? x) ...)

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Unnecessary into

  • Description: This code smell occurs when the into function is used in situations where more concise or idiomatic alternatives exist, leading to unnecessarily verbose or inefficient code. While into is useful for combining collections, it is often misused for simple type transformations—such as (into [] coll) instead of vec.

  • Example:

;; Example from source

(into [] xs)

(into #{} xs)

(into {} (map (fn [[k v]] [k (f v)]) m))

(into {} (for [[k v] m] [k (f v)]))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Module Boundaries & Data Contracts

This category focuses on the surface area of namespaces and modules, as well as on the clarity and predictability of the values exchanged across them.

Monolithic Namespace Split

  • Description: The practice of splitting a single logical namespace across multiple physical files using the legacy, imperative macros load and in-ns. It breaks static analysis and build tools on dependency resolution, leading to fragile code that is difficult to manage. This smell should be replaced by creating separate, distinct namespaces and managing them explicitly with require.

  • Example:

;; Example from source
(ns slamhound-test.core)
(load "core_extra.clj")
(defn -main [& args]
  (pprint args)
  (io/copy (ByteArrayInputStream. (.getBytes "hello"))
           (first args)))

(in-ns 'slamhound-test.core)
(defn temp [])

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Implicit Namespace Dependencies

  • Description: This smell occurs when code relies on namespaces whose dependencies are not made explicit through clear and precise :require declarations. This may happen through broad imports such as :use or :refer :all, which obscure the origin of symbols, or by relying on namespaces that happen to be loaded indirectly by other libraries or by Clojure's runtime implementation. Such practices create hidden coupling, reduce code clarity, and make static analysis, refactoring, and maintenance more difficult. They may also lead to symbol conflicts, namespace pollution, or code that depends on implementation details rather than explicit declarations.

  • Example:

(ns foo
  (:require [clojure.string :refer :all]) ; imports all symbols from clojure.string
  (:use clojure.set))                     ; makes all symbols from clojure.set available

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Marker Protocol

  • Description: The use of defprotocol solely to define a type identifier (a "marker") for use with type checking, rather than defining a contract for polymorphic behavior. The defprotocol macro is intended for defining methods that can be extended to different types. Making it a marker introduces the complexity and overhead of the protocol machinery unnecessarily.

  • Example:

;; Example from source
  clojure.lang.IEquiv
  (-equiv [_ other]
    (and (satisfies? IUUID other)
         (identical? uuid (.-uuid other))))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Single-segment Namespace

  • Description: This structural anti-pattern occurs when a project uses single-segment namespaces (e.g., digest or config instead of my-app.digest or my-app.config). This practice violates the standard, hierarchical naming convention of the Clojure ecosystem, causing issues: it increases the risk of global naming collisions, reduces the clarity of code organization, and frequently leads to tooling errors that rely on predictable, qualified names.

  • Example:

;; Not from source
(ns digest) 
(defn md5 [s] ...)

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Private Multimethods

  • Description: The use of a private function (defn-) or metadata to define a defmulti or its defmethods. Multimethods are designed for dynamic, open polymorphism and external extension. Making them private defeats their architectural purpose, forces the abstraction to behave as a closed system, and introduces unnecessary complexity and overhead.

  • Example:

;; Example from source
(defmulti ^:private indenter-fn
  "Multimethod for applying indentation rules to forms."
  ;; Accepts [rule-key list-indent-size [rule-type & args]]
  (fn dispatch
    [_ _ rule]
    (first rule)))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Non-Idiomatic Record Construction

  • Description: This smell occurs when a developer uses Java interop constructor syntax (e.g., (Foo. 1 2)) to instantiate a defrecord or deftype. Since Clojure automatically generates constructor functions (->Foo and map->Foo for records), using interop syntax is considered non-idiomatic and bypasses the standard abstractions provided by the language. Prefer constructor functions or dedicated factory functions instead.

  • Example:

(defrecord Foo [a b])

(Foo. 1 2)
;;=> #user.Foo{:a 1, :b 2}

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Excessive Refers

  • Description: Occurs when a namespace explicitly refers a large number of Vars (or uses the anti-pattern (:refer :all)) from another namespace. This practice leads to Namespace Pollution, drastically increases the risk of name collisions with other libraries or future code, and makes the source of any function call ambiguous.

  • Example:

;; Not from source
(ns my-app.core
  (:require [my-lib.utils
             :refer [this
                     that
                     the
                     other
                     more
                     moar]])) ; <--- Long list of referred symbols

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Map With Nil Values

  • Description: This code smell occurs when nil values are inserted into a map. In Clojure, both a missing key and a key explicitly associated with nil return nil when accessed, making it difficult to distinguish between the two cases. This ambiguity can obscure program intent, lead to subtle bugs, and complicate reasoning about data state. Instead of inserting nil, prefer omitting the key entirely or using a sentinel value that more clearly expresses the intended meaning.

  • Example:

(defn welcome-message [user]
  (str "Welcome, " (:name user)))

(welcome-message {:id 42})
(welcome-message {:id 43 :name nil})

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Non-Idiomatic Parameter Binding

  • Description: The use of confusing or non-standard syntax (such as & [x]) to define a single optional argument or variadic arity. This binding method is verbose, structurally confusing (as it captures a single argument from a sequence), and obscures the function's contract. This must be refactored by replacing the complex binding with the explicit, idiomatic practice of using multiple function arities or a clean options map.

  • Example:

;; Not from source
(defn- CustomExtension-make [project & [ns]]
  (if ns
    (do-stuff project ns)
    (do-stuff project nil)))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Logic Flow & Readability

This category focuses on the visual and cognitive path through which data transformations are expressed in code.

Conditional Build-Up

  • Description: This code smell occurs when a state is incrementally constructed through a series of let, if, and assoc expressions, leading to verbose and imperative-style code. Rather than clearly expressing the transformation logic, this pattern scatters conditional state mutations across multiple branches, making it harder to reason about the overall flow.

  • Example:

;; Not from source
(let [m {}
      m (if (:a input) (assoc m :a 1) m)
      m (if (:b input) (assoc m :b 2) m)
      m (if (:c input) (assoc m :c 3) m)
      m (if (:d input) (assoc m :d 4) m)
      m (if (:e input) (assoc m :e 5) m)]
  m)

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Nested Forms

  • Description: This code smell occurs when multiple binding or iteration forms—such as let, when-let, if-let, or doseq—are unnecessarily nested instead of being combined in a single, flat form. While technically valid, this nesting introduces extra indentation and structural complexity without adding semantic value. It obscures the relationships between bindings, increases visual noise, and makes the code harder to read and reason about.

  • Example:

(defn process [user]
  (let [profile (:profile user)]
    (when profile
      (let [address (:address profile)]
        (when address
          (let [city (:city address)]
            (str "City: " city)))))))

(process {:profile {:address {:city "Recife"}}})

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Misused Threading

  • Description: The misuse of threading macros (-> or ->>) to chain together operations where the data type of the threaded argument changes fundamentally at each step (e.g., threading a map into a string, into a File object, and back into a map). Threading macros are intended for homogeneous, sequential transformations on a similar data type.

  • Example:

;; Example from source
(defn read-project-raw [project]
  (-> project
      (:root)
      (io/file "project.clj")
      (str)
      (project/read-raw)))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Case with Non-Literal Test Values

  • Description: This smell occurs when the case macro is used with test expressions that rely on runtime values (such as bindings or variables) instead of compile-time constants (literals). The case macro is optimized to test for the identity of literals and does not guarantee correct runtime equality (=) for dynamic values. Using it with non-literal values is a dangerous practice that can lead to subtle, difficult-to-debug logic errors. Developers must use cond or condp for all runtime comparison logic.

  • Example:

;; Not from source
(defn MyCameraView [current-orientation]
  (case result
    "portrait"  (do-portrait-logic)
    "landscape" (do-landscape-logic)
    :else       (do-default-logic)))

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Thread Ignorance

  • Description: This code smell occurs when developers avoid or misuse Clojures threading macros (->, ->>, some->, cond->) in scenarios where they would provide clearer, more idiomatic data flow. Instead of leveraging threading to express stepwise transformations, code may fall back to repetitive bindings, deeply nested let or when-let forms, or manually sequenced function calls. This results in verbose, harder-to-follow logic and obscures the intent behind each transformation.

  • Example:

(defn transform [xs]
  (let [step1 (map inc xs)
        step2 (filter even? step1)
        step3 (reduce + step2)]
    step3))

(transform [1 2 3 4])

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Verbose Checks

  • Description: This code smell arises when developers manually implement common checks (such as checking if a number is zero, positive, or negative), when Clojure already provides clear, idiomatic functions that do the same. This results in verbose and less readable code, and misses an opportunity to leverage Clojure's built-in abstractions for clarity.

  • Example:

(defn number-type [n]
  (cond
    (= n 0) :zero
    (< 0 n) :positive
    (> 0 n) :negative))

(number-type 0)
;; => :zero

(number-type 5)
;; => :positive

(number-type -3)
;; => :negative

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Redundant do block

  • Description: This code smell occurs when developers wrap expressions in a (do ...) block inside constructs that already support an implicit do, such as let, when, fn, try, and others. This redundant use of do adds no semantic value but introduces unnecessary syntax, making the code appear more complex and imperative than it actually is.

  • Example:

(defn process-item [x]
  (when (pos? x)
    (do
      (println "Processing:" x)
      (* x 2))))

(process-item 2)

Sources and Excerpts: View sources and supporting excerpts

↑ Back to table of contents ↑


Methodology

The methodology follows the original study on Elixir by Vegi & Valente (2023), with adaptations for the specifics of Clojure.

The figure below summarizes the overall research process:


In a nutshell, our study started with a systematic identification phase combining searches on Google and GitHub. Through Google searches using keywords related to Clojure and code smells, we located community discussions, blog posts, forums, and other practitioner-oriented sources. In parallel, we mined GitHub repositories from the Clojure ecosystem, analyzing issues, pull requests, commits, and source code artifacts. After a document selection process, we examined the collected material to identify recurring problematic patterns reported or exhibited by Clojure developers. This analysis resulted in the first version of the code smell catalog.

Next, we promoted the catalog through the main communication channels of the Clojure community and collected feedback through a questionnaire. We analyzed practitioners comments and suggestions to refine the catalog, improving the description, relevance, and scope of the identified smells.

To further validate the catalog, we conducted a survey involving 95 Clojure developers and organized a Clojure Guild session with influential practitioners. The feedback obtained during this phase was analyzed and incorporated into the final version of the catalog.

↑ Back to table of contents ↑


Contributions are welcome via Issues and Pull Requests.