How to choose between protocols and multimethods for polymorphism in Clojure, and what each trade-off means in real code.
Protocol: A named set of operations that dispatches primarily by the concrete type of the first argument.
Multimethod: A dispatch mechanism that chooses behavior from an arbitrary dispatch function, not only from concrete type.
Protocols and multimethods are two of Clojure’s main tools for polymorphism, but they solve different problems. If you treat them as interchangeable, the code usually becomes either too rigid or too dynamic for the actual need.
The practical question is simple: do you want behavior chosen mainly by type, or by a richer classification rule? Protocols are strongest when you want fast, stable, type-oriented operations. Multimethods are stronger when dispatch depends on values, categories, or several dimensions of context.
Protocols work well when you have a clear conceptual capability that several types should implement.
1(defprotocol Billable
2 (monthly-cost [this])
3 (active? [this]))
That protocol says, “Anything that is billable must answer these operations.” The call sites stay compact:
1(monthly-cost plan)
2(active? subscription)
The main strengths are:
In practice, protocols fit well when the type itself is the important decision surface.
Clojure records and protocols work naturally together because records give the system concrete types and protocols define the shared operations.
1(defprotocol Shape
2 (area [this])
3 (perimeter [this]))
4
5(defrecord Rectangle [width height])
6(defrecord Circle [radius])
7
8(extend-type Rectangle
9 Shape
10 (area [{:keys [width height]}]
11 (* width height))
12 (perimeter [{:keys [width height]}]
13 (* 2 (+ width height))))
14
15(extend-type Circle
16 Shape
17 (area [{:keys [radius]}]
18 (* Math/PI radius radius))
19 (perimeter [{:keys [radius]}]
20 (* 2 Math/PI radius)))
That is a strong fit because the behavior is truly attached to the type itself. A rectangle is not “sometimes” a rectangle depending on runtime data. The dispatch story is stable.
Multimethods are better when behavior should not be tied only to one concrete type.
For example, imagine processing events by both :domain and :action:
1(defmulti handle-event
2 (fn [event]
3 [(:domain event) (:action event)]))
4
5(defmethod handle-event [:billing :invoice-issued]
6 [event]
7 {:topic :customer-email
8 :payload {:invoice-id (:invoice-id event)}})
9
10(defmethod handle-event [:security :password-reset]
11 [event]
12 {:topic :audit-log
13 :payload {:user-id (:user-id event)}})
This is not primarily about the concrete JVM type of event. It is about semantic classification. A multimethod handles that cleanly because the dispatch function can choose whatever dimension matters.
A good rule of thumb:
That difference matters more than syntax.
If your system already has records representing distinct domain entities and you want shared operations on them, a protocol is often the better fit.
If the data is map-shaped, open-ended, or routed by domain tags, multimethods often read better and require less ceremony.
Multimethods are flexible because their dispatch function can be anything:
That flexibility is valuable, but it also means the dispatch rule itself becomes part of the design surface. Readers must understand both the method implementations and the rule that chooses among them.
Protocols are narrower. That is often a benefit. They constrain the design so readers know dispatch is about concrete type.
A common mistake is reaching for protocols or multimethods when a map of handlers or a small conditional would be simpler.
For example, this is often enough:
1(def handlers
2 {:started on-started
3 :stopped on-stopped
4 :failed on-failed})
5
6((get handlers (:status event) default-handler) event)
That may be clearer than introducing a full dispatch abstraction if the behavior surface is small and stable.
The right question is not “Which polymorphism feature can I use?” It is “What is the cleanest readable dispatch model for this problem?”
Protocols tend to fit better when the set of supported operations is stable and types are meaningful architectural units.
Multimethods tend to fit better when new categories or dispatch combinations appear over time and type alone is not the right selector.
This is a useful mental model:
flowchart TD
A["Need dispatch"] --> B{"Behavior mainly tied to concrete type?"}
B -->|Yes| C["Prefer protocol"]
B -->|No| D{"Dispatch depends on values, categories, or several dimensions?"}
D -->|Yes| E["Prefer multimethod"]
D -->|No| F["Plain functions, maps, or conditionals may be enough"]
The point is not to force every case into a framework. It is to make the dispatch choice explicit.
Protocols are usually the better choice when you need a fast, straightforward polymorphic layer that sits on hot paths or foundational interfaces.
Multimethods are often worth their extra overhead when:
Most applications do not need hyper-optimization here, but performance still belongs in the decision when a dispatch abstraction becomes central.