Multimethods with `defmulti` and `defmethod`

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.

The Basic Shape

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:

  • computes a dispatch value using the function from defmulti
  • looks for the most appropriate method
  • falls back to the default dispatch value if present
  • throws if no method matches

That dispatch value can be a keyword, a class, a vector, or something derived through Clojure’s hierarchy system.

Why Use a Multimethod Instead of a Protocol?

Protocols are generally best when:

  • dispatch is about the type of the first argument
  • the behavior belongs naturally to a type-like interface
  • you care about a narrower, faster dispatch model

Multimethods are better when:

  • dispatch depends on values, not only types
  • dispatch depends on multiple properties
  • you want custom hierarchies with derive and isa?
  • extension should remain open-ended across namespaces
  • flexibility matters more than raw dispatch speed

That trade-off is the core design point. Multimethods are broader and more dynamic than protocols, but the extra flexibility comes with extra indirection.

Dispatch Can Express Policy

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.

Hierarchies Make Multimethods More Expressive

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.

Ambiguity Is a Design Signal

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.

When Not to Use Multimethods

Avoid multimethods when:

  • a plain case or cond already expresses the rule clearly
  • the behavior is tightly tied to a type and should probably be a protocol
  • the dispatch rules are local and unlikely to grow
  • the added indirection would make debugging or onboarding harder

An idiomatic rule of thumb is:

  • ordinary functions and conditionals first
  • protocols for type-oriented polymorphism
  • multimethods when dispatch policy is genuinely about values, relationships, or hierarchies

Dispatch Flow

    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.

Quiz

Loading quiz…
Revised on Thursday, April 23, 2026