Learn what a multiton really is in Clojure, why a keyed registry is usually the pattern's core, and how dynamic vars can help with scoped selection but should not be mistaken for the registry itself.
Multiton pattern: A keyed registry that manages a bounded set of distinct shared instances instead of one global singleton.
The old object-oriented explanation of multiton is straightforward: instead of one global instance, you keep one instance per key. The part that needs correction in Clojure is the role of dynamic vars. Dynamic vars are useful for scoped contextual selection, but they are usually not the multiton itself.
In Clojure, a simple multiton usually looks like a registry or lifecycle-managed map:
1(defonce instances (atom {}))
2
3(defn create-instance [k]
4 {:instance/key k
5 :created-at (System/currentTimeMillis)})
6
7(defn get-instance [k]
8 (or (get @instances k)
9 (let [instance (create-instance k)]
10 (get (swap! instances #(if (contains? % k) % (assoc % k instance))) k))))
That is the essence of multiton:
Dynamic vars are most useful when code needs a scoped notion of the “current” instance for this thread or request.
1(def ^:dynamic *current-tenant* nil)
2
3(defn current-instance []
4 (when *current-tenant*
5 (get-instance *current-tenant*)))
6
7(binding [*current-tenant* :tenant-a]
8 (current-instance))
Here the dynamic var does not store the registry. It stores the current key selection for a scoped execution context.
That is a much better fit for dynamic vars:
That design confuses two very different concerns:
Dynamic vars are not a substitute for explicit lifecycle management. They are a contextual-selection tool.
If the instances are expensive resources such as:
then the registry also needs:
Otherwise multiton quietly turns into a resource leak with a nice pattern name.
The health of a multiton often depends less on the lookup function and more on whether the key space is intentionally bounded. Tenant identifiers, region names, or a short list of service roles are manageable. User IDs, request IDs, or arbitrary payload-derived keys usually are not.
When the key space can grow, the design needs one of these answers:
This mixes scoped binding with shared instance management and makes the design harder to reason about.
If keys are not bounded or lifecycle is ignored, memory and resource use can drift upward indefinitely.
Sometimes a registry is unnecessary and explicit dependency passing is clearer.
Use a multiton when the system genuinely needs one shared instance per meaningful key. Represent that with an explicit registry. Use dynamic vars only when code also needs a scoped current selection such as the active tenant or environment. In Clojure, multiton works best when the registry and the scoped binding are treated as related but distinct tools.