Learn what Clojure’s dynamic typing really means, how polymorphism works through functions, protocols, and multimethods, and how to design flexible code without losing clarity.
Clojure is dynamically typed, but that does not mean “types do not matter.” It means types belong to values at runtime rather than being fixed in variable declarations. That design encourages flexible generic code, but it also shifts some responsibility from the compiler to the programmer’s data modeling and API design.
Dynamic typing: A model where values carry types at runtime, while variables and parameters are not declared with fixed types in advance.
Understanding that clearly is important because many newcomers hear “dynamic typing” and assume anything can be mixed with anything safely. That is not how Clojure works. Operations still have expectations. They are just enforced at runtime instead of through mandatory declared types.
This is valid Clojure:
1(def x 42)
2(def x "now a string")
The name is rebound to a different value. The language does not require a static type declaration for x.
But functions still operate meaningfully only on appropriate inputs:
1(+ 2 3)
2;; => 5
3
4(+ "hello" "world")
5;; error
Dynamic typing does not erase semantic expectations. It changes when and how those expectations are enforced.
One of Clojure’s strengths is that many functions work across broad families of data rather than across tightly declared class hierarchies.
For example, sequence functions work with many collection types:
1(map inc [1 2 3])
2(map inc '(1 2 3))
3(map inc #{1 2 3})
This kind of genericity often comes from shared interfaces and protocols implemented by the runtime, not from the caller declaring types ahead of time.
In Clojure, polymorphism does not mean only “call a method on an object.” It can take several forms:
Choosing among these is a design decision.
Many problems that look like polymorphism do not need a polymorphism framework.
1(defn full-name [{:keys [first-name last-name]}]
2 (str first-name " " last-name))
If the data shape is already enough, a plain function is often the most idiomatic choice. Clojure code gets simpler when behavior is expressed directly over data.
Protocols are the right tool when behavior is tied to a type-like abstraction and dispatch should happen on the first argument’s type.
1(defprotocol Drawable
2 (draw [this]))
3
4(defrecord Circle [radius]
5 Drawable
6 (draw [_] (str "Circle with radius " radius)))
Protocols are useful because they:
They are not the only form of polymorphism, and they are not automatically needed whenever behavior varies.
Sometimes behavior depends on more than type. It may depend on a keyword, a category, a status, or a custom hierarchy.
1(defmulti handle-event :kind)
2
3(defmethod handle-event :created [event]
4 :persist)
5
6(defmethod handle-event :deleted [event]
7 :archive)
Multimethods are especially useful when:
derive hierarchies help model the domainThey are more flexible than protocols, but usually less direct and heavier than plain functions.
Because many guarantees are not enforced at declaration time, good Clojure design relies more on:
Dynamic typing becomes dangerous mostly when boundaries are muddy and assumptions are left implicit.
graph TD;
A["Behavior varies"] --> B{"Plain function over data enough?"}
B -->|Yes| C["Use ordinary functions"]
B -->|No| D{"Dispatch by first-arg type?"}
D -->|Yes| E["Use a protocol"]
D -->|No| F["Use a multimethod or other value-based dispatch"]
The diagram below captures the most useful habit in Clojure: start simple, then move to protocols or multimethods only when the dispatch pressure is real.