Manage shared lookups carefully in Clojure with atoms and maps without turning the registry into a hidden service locator.
A registry pattern stores a set of named values or operations behind a central lookup table. In Clojure, the usual representation is simple: a map, sometimes wrapped in an atom when registrations must change at runtime.
That simplicity is attractive, but it also makes the pattern easy to misuse. A registry can be a helpful runtime lookup boundary, or it can quietly become a service locator that hides half the system behind global keys.
A registry makes sense when you truly need late-bound lookup by name or key. Common examples include:
In those cases, the indirection is part of the domain. The lookup is not accidental.
1(ns app.codec)
2
3(defonce codecs
4 (atom {}))
5
6(defn register-codec! [format encode-fn]
7 (swap! codecs assoc format encode-fn))
8
9(defn encode [format value]
10 (let [encode-fn (get @codecs format)]
11 (when-not encode-fn
12 (throw (ex-info "No codec registered"
13 {:type ::missing-codec
14 :format format})))
15 (encode-fn value)))
This design is coherent because the lookup key, format, is part of the problem itself. Callers are explicitly asking for behavior by name.
An atom is appropriate when registrations are independent updates to one shared map. That fits many registry cases:
Each change is local and atomic. You are not coordinating a large multi-reference transaction. That makes swap! and a map a good fit.
The pattern becomes dangerous when it starts standing in for explicit dependency wiring.
Bad signs include:
At that point, the registry is no longer a focused lookup table. It is becoming a hidden architecture.
Use dependency injection when a component knows exactly which collaborator it needs.
Use a registry when lookup by key is genuinely part of the design.
That distinction matters. If the caller already knows it needs send-email!, passing that dependency explicitly is clearer than asking a registry for :email-service. But if the caller receives a message type at runtime and must choose the correct handler dynamically, a registry may be exactly right.
Registries also need ownership rules:
If those questions are not answered, the registry becomes a source of hidden coupling and brittle tests.
The first mistake is registering everything. A registry should represent a real lookup surface, not become the storage closet for unrelated application state.
The second mistake is giving it vague keys. If names are not stable and well-documented, the indirection becomes fragile.
The third mistake is pretending a registry is harmless because it uses immutable maps internally. The atom still introduces shared runtime state, and the architectural risks remain.
Ask these before approving a registry:
If the lookup is real and the lifecycle is disciplined, the pattern can work well. If not, reduce it to explicit injection.