Learn when protocols are the right polymorphism tool in Clojure, how they differ from multimethods and records alone, and how to extend them safely.
Protocol: A named set of method signatures that dispatch on the type of the first argument, giving Clojure a high-performance, open-ended way to express interface-style polymorphism.
Protocols matter because they let you describe an abstraction without forcing everything into an inheritance hierarchy. They are a strong fit when the main dispatch question is simple type-based behavior and performance or interop matters.
Protocols work best for the common case of single dispatch on type. They give you:
That last point is important. According to the official docs, protocols were designed partly to avoid the problem where only the original type author gets to decide which interfaces a type implements.
1(defprotocol Shape
2 (area [shape])
3 (perimeter [shape]))
This creates:
The main semantic rule is that dispatch happens on the type of the first argument.
Protocols pair naturally with records and types:
1(defrecord Rectangle [width height]
2 Shape
3 (area [_] (* width height))
4 (perimeter [_] (* 2 (+ width height))))
This is a clean fit when you control the type and the behavior genuinely belongs with that representation.
One of the real strengths of protocols is that you can extend a type later:
1(extend-type java.lang.String
2 Shape
3 (area [s] (count s))
4 (perimeter [s] (* 2 (count s))))
This is more flexible than host-language interfaces, but it comes with a design warning. The official protocol reference explicitly advises caution when you own neither the protocol nor the type. In library code, that can create conflicts. In app code, it is usually more acceptable because you control the local environment.
A common mistake is reaching for protocols when the dispatch rule is not really about type.
Use a protocol when:
Use a multimethod when:
Protocols are not a replacement for every form of polymorphism.
You can absolutely write ordinary functions that dispatch on keys or values manually, and sometimes that is the better call. Protocols earn their keep when the abstraction is stable enough to deserve a named interface and the participating types are genuinely different implementations of that interface.
That means protocols are often strong for:
They are weaker when the behavior is just a few branches over ordinary data values.
flowchart TD
A["Need polymorphism"] --> B{"Dispatch mainly on type of first argument?"}
B -- Yes --> C["Protocol is a strong candidate"]
B -- No --> D["Consider multimethods or ordinary functions"]
Protocols work best when the methods belong together conceptually.
Bad protocol design often looks like:
A good protocol usually answers one clear question, such as “how does this thing render?” or “how do I persist this target?”