Why extra layers, speculative interfaces, and clever indirection often make Clojure systems harder to change instead of more elegant.
Unnecessary abstraction is an anti-pattern because it adds indirection before the code has earned it. In Clojure, this often appears as extra protocols, multimethods, macros, wrappers, or tiny internal DSLs introduced to make the design feel elegant. Instead of making the system clearer, these layers hide behavior behind names that sound general but explain very little.
The core problem is not abstraction itself. The problem is abstraction that arrives earlier than the actual variability, complexity, or reuse pressure that would justify it.
One of the most common forms is speculative architecture:
This feels prudent because it seems to prepare the codebase for growth. In practice it often means:
The anti-pattern is designing around hypothetical change instead of observed change.
Clojure is strong at expressing behavior directly with data and small functions. Unnecessary abstraction often appears when ordinary data shapes are wrapped in layers of constructors, dispatch helpers, and protocol surfaces that do not add real meaning.
Weak:
1(defprotocol UserLookup
2 (fetch-user [this id]))
3
4(defrecord UserLookupService [db]
5 UserLookup
6 (fetch-user [_ id]
7 (jdbc/execute-one! db ...)))
If the codebase has one implementation and no meaningful polymorphic pressure, a plain function is often clearer:
1(defn fetch-user [db id]
2 (jdbc/execute-one! db ...))
The abstraction layer is only worth keeping if it makes the system easier to understand or extend in a real way.
Some teams start building little internal languages whenever repeated patterns appear. In Clojure this often happens with:
Sometimes these are useful. Often they hide basic business logic behind a clever execution model that few readers fully understand.
If a domain-specific layer requires constant explanation, the abstraction may be extracting complexity from the call site while concentrating it into a smaller but more dangerous area.
Another abstraction smell is the “relay race” architecture:
Each layer may do almost nothing besides renaming the call.
This hurts because:
Abstraction should compress complexity, not lengthen the path to it.
Developers often notice two similar call sites and immediately generalize them into a shared abstraction. That is sometimes correct, but it is also a common trap.
Two similar pieces of code may differ in ways that matter later. If generalized too early, the new abstraction becomes:
In Clojure, small local duplication is often cheaper than a premature generic layer.
Clojure already gives you:
An anti-pattern appears when the code starts rebuilding these capabilities inside the application. Instead of leaning on the language, the system creates an application-specific framework that imitates it.
This is closely related to the inner platform effect, but at a smaller scale it often looks like “micro-frameworking” ordinary application code.
flowchart TD
A["See repeated or similar code"] --> B{"Is the current version already hard to understand or change?"}
B -->|No| C["Leave it simple for now"]
B -->|Yes| D{"Is the variability real and stable?"}
D -->|No| E["Refactor locally, avoid generalization"]
D -->|Yes| F["Introduce the smallest abstraction that clarifies the design"]
This model is helpful because it treats abstraction as an earned move, not a reflex.