Common Clojure polymorphism mistakes, including when protocols or multimethods add more abstraction cost than value.
Misusing protocols and multimethods usually means reaching for a dispatch abstraction before the real dispatch problem is clear. The result is not elegant polymorphism. It is extra indirection, weaker readability, and more architecture than the program actually needs.
This anti-pattern appears in two directions:
The shared problem is not “using the wrong syntax.” It is adding a polymorphism mechanism that does not match the shape of the decision.
Protocols make sense when several types share a capability boundary. They make much less sense when there is one type and no real extension story.
Weak:
1(defprotocol ReportRunner
2 (run-report [this]))
3
4(defrecord DailySalesReport [db])
5
6(extend-type DailySalesReport
7 ReportRunner
8 (run-report [{:keys [db]}]
9 ...))
If there is only one meaningful implementation and no evidence that the abstraction will open up, this is usually just ceremony.
A plain function is often clearer:
1(defn run-daily-sales-report [db]
2 ...)
The anti-pattern here is speculative abstraction. The code pays the complexity cost immediately while the flexibility never actually arrives.
Sometimes the real dispatch dimension is not type. It is a value in the data.
Example:
1{:event-type :invoice-issued ...}
2{:event-type :user-locked ...}
Trying to force this into protocols can create an awkward architecture where record types exist mainly to satisfy the dispatch tool rather than the domain.
In that case, a multimethod or even a handler map may be the cleaner choice because the classification is semantic, not type-based.
The warning sign is simple: if you are inventing types just so protocols can dispatch, you may be solving the wrong problem.
Multimethods are flexible, but that flexibility has a cost. When the dispatch space is tiny and closed, a multimethod can be heavier than necessary.
Weak:
1(defmulti status-label :status)
2
3(defmethod status-label :ok [_] "OK")
4(defmethod status-label :error [_] "Error")
5(defmethod status-label :pending [_] "Pending")
That may be acceptable, but a small lookup is often simpler:
1(def status->label
2 {:ok "OK"
3 :error "Error"
4 :pending "Pending"})
5
6(defn status-label [{:keys [status]}]
7 (get status->label status "Unknown"))
The anti-pattern is using a heavyweight dispatch abstraction when a plain data lookup communicates the behavior more directly.
A dispatch rule should be readable enough that a teammate can explain it without digging through several helpers.
If a multimethod dispatch function becomes something like:
then the dispatch mechanism itself may be becoming the problem.
Complex dispatch is sometimes legitimate, but it should reflect real domain complexity, not accidental cleverness.
Not every case, cond, or handler map needs to become a protocol or multimethod.
This is often enough:
1(defn normalize-input [value]
2 (cond
3 (string? value) (clojure.string/trim value)
4 (keyword? value) (name value)
5 :else value))
Turning a small, obvious branch like this into a reusable dispatch framework usually makes the code harder to follow, not easier.
The question is not “Can I replace this conditional?” It is “Would the replacement make the design more honest?”
Multimethods are often fine. But on a hot path, using them casually can be a real mistake if the system would be better served by:
The anti-pattern here is assuming the flexibility is free. If the code sits on a measured hot path, performance should be part of the design decision.
One of the most subtle problems is using these abstractions because they feel sophisticated.
Symptoms:
This is especially common in early refactors, where a simple system is given a framework-like dispatch layer too early.
flowchart TD
A["Need behavior selection"] --> B{"Plain function, conditional, or data map already clear?"}
B -->|Yes| C["Keep it simple"]
B -->|No| D{"Decision mainly based on concrete type?"}
D -->|Yes| E["Protocol may fit"]
D -->|No| F{"Decision based on values or categories?"}
F -->|Yes| G["Multimethod may fit"]
F -->|No| H["Re-examine the abstraction"]
This is not about avoiding protocols or multimethods. It is about using them for the shape of problem they actually solve.