Learn when Clojure multimethods are better than protocols or plain conditionals, and how dispatch values, hierarchies, defaults, and prefer-method shape flexible runtime polymorphism.
Protocols are often the first polymorphism tool Clojure developers learn, but they are not the only one. Sometimes dispatch depends on more than the type of the first argument. Sometimes you want open extension over values, attributes, or custom hierarchies that are not tied to one record or Java class. That is where multimethods matter.
Multimethod: A polymorphic function whose implementation is chosen by a dispatch function, not just by the class of the first argument.
The official Clojure reference describes multimethods as runtime polymorphism over values, attributes, metadata, and relationships between one or more arguments. That flexibility is the reason to use them. It is also the reason not to reach for them casually.
defmulti defines the dispatching function. defmethod attaches implementations to dispatch values.
1(defmulti area :shape)
2
3(defmethod area :circle [{:keys [radius]}]
4 (* Math/PI radius radius))
5
6(defmethod area :rectangle [{:keys [width height]}]
7 (* width height))
8
9(defmethod area :default [_]
10 (throw (ex-info "Unsupported shape" {})))
When you call area, Clojure:
defmultiThat dispatch value can be a keyword, a class, a vector, or something derived through Clojure’s hierarchy system.
Protocols are generally best when:
Multimethods are better when:
derive and isa?That trade-off is the core design point. Multimethods are broader and more dynamic than protocols, but the extra flexibility comes with extra indirection.
The dispatch function can compute whatever classification the design needs.
1(defmulti route-event
2 (fn [event]
3 [(:kind event) (:priority event)]))
4
5(defmethod route-event [:alert :high] [_]
6 :page-immediately)
7
8(defmethod route-event [:alert :low] [_]
9 :queue-for-review)
10
11(defmethod route-event [:audit :high] [_]
12 :persist-and-notify)
That kind of cross-cutting dispatch is awkward with protocols and often cleaner than an ever-growing cond when the rules genuinely form an extension point.
One of the official reference strengths is support for ad hoc hierarchies using derive and isa?.
1(derive ::dog ::animal)
2(derive ::cat ::animal)
3
4(defmulti speak :type)
5
6(defmethod speak ::animal [_]
7 "generic animal sound")
8
9(defmethod speak ::dog [_]
10 "woof")
If an input has :type ::dog, the dog-specific method wins. If an input uses a derived type without its own method, a parent method can still match.
This is useful for domain taxonomies such as product categories, event families, or policy classes that do not map cleanly onto Java inheritance.
Because multimethod dispatch can be flexible, ambiguity can happen. Two methods may both appear applicable. Clojure gives you prefer-method for these cases.
That is useful, but it is also a warning signal. If you regularly need prefer-method, your hierarchy or dispatch function may be encoding too many competing concerns. Sometimes a simpler classification model or a smaller dispatch surface is the better fix.
Avoid multimethods when:
case or cond already expresses the rule clearlyAn idiomatic rule of thumb is:
graph TD;
A["Call multimethod"] --> B["Run dispatch function"]
B --> C{"Exact or derived match?"}
C -->|Yes| D["Run matching method"]
C -->|No| E{"Default method present?"}
E -->|Yes| F["Run default method"]
E -->|No| G["Throw dispatch error"]
The diagram below captures the core idea: a multimethod is dispatch policy plus independently extensible implementations.