Clojure Protocols and Multimethods

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 Are for Stable Type-Shaped Behavior

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:

  • clear, named capability boundaries
  • direct type-based dispatch
  • readable call sites
  • good performance for routine polymorphic operations

In practice, protocols fit well when the type itself is the important decision surface.

Records Plus Protocols Often Form a Clean Pair

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 for Richer Classification Rules

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.

The Real Choice: Type Identity vs Domain Classification

A good rule of thumb:

  • choose a protocol when the core question is “What operations does this type support?”
  • choose a multimethod when the core question is “How should I route behavior based on a classification rule?”

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.

Dispatch Flexibility Has a Cost

Multimethods are flexible because their dispatch function can be anything:

  • one keyword
  • a vector of values
  • a derived hierarchy
  • some other computed classification

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.

Neither Tool Replaces Plain Functions

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 Favor Closed, Multimethods Favor Open Classification

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.

Performance and Operational Simplicity

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:

  • the dispatch rule is genuinely more expressive
  • correctness and extensibility matter more than raw dispatch speed
  • the domain is easier to understand as classified data than as distinct record types

Most applications do not need hyper-optimization here, but performance still belongs in the decision when a dispatch abstraction becomes central.

Common Mistakes

  • using multimethods when ordinary type-based dispatch would be simpler
  • forcing protocols onto loosely tagged map data where type is not the real issue
  • creating one abstraction too early before the dispatch shape is clear
  • treating “more dynamic” as automatically “more idiomatic”
  • hiding simple routing logic behind heavyweight polymorphism

Key Takeaways

  • Protocols are strongest for stable, type-oriented capabilities.
  • Multimethods are strongest for value-oriented or multi-dimensional dispatch.
  • Records plus protocols are often a clean pairing.
  • Open-ended map data often pairs better with multimethods or plain handler maps.
  • The simplest readable dispatch model is usually the right one.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026