How Clojure services participate in modern distributed systems with platform DNS, registries such as Consul, and selective coordination services.
Distributed systems and service discovery are easiest to reason about when you separate three different concerns:
Older material often treats ZooKeeper as the default answer to all three. That is no longer a good default. In modern systems, service discovery is often provided by the runtime itself, especially in Kubernetes, or by a registry/catalog platform such as Consul. Coordination systems still matter, but they solve a narrower class of problems than “how do services find each other?”
In most current deployments, the application should not own the full discovery mechanism. It should consume discovery through a stable platform contract.
Common patterns include:
The best Clojure code often knows very little about the discovery internals. It usually needs:
That keeps discovery logic from leaking into every namespace.
Kubernetes Services create stable service identities, and cluster DNS resolves those names for clients. For many systems, that is enough. The application only needs to call a service name that the runtime resolves.
1(ns acme.checkout.pricing
2 (:import [java.net URI]
3 [java.net.http HttpClient HttpRequest HttpResponse]
4 [java.time Duration]))
5
6(def ^HttpClient client
7 (-> (HttpClient/newBuilder)
8 (.connectTimeout (Duration/ofSeconds 2))
9 (.build)))
10
11(defn pricing-base-url []
12 (or (System/getenv "PRICING_URL")
13 "http://pricing.default.svc.cluster.local:8080"))
14
15(defn fetch-price [sku]
16 (let [request (-> (HttpRequest/newBuilder
17 (URI/create (str (pricing-base-url) "/prices/" sku)))
18 (.timeout (Duration/ofSeconds 3))
19 (.GET)
20 (.build))
21 response (.send client request (HttpResponse$BodyHandlers/ofString))]
22 {:status (.statusCode response)
23 :body (.body response)}))
This works well when the platform already handles:
In VM-heavy, hybrid, or multi-runtime systems, a registry such as Consul can act as a central catalog of services and their locations. In that model, clients or sidecars resolve healthy instances through the registry’s DNS or HTTP APIs.
The application still should not scatter registry calls everywhere. Wrap them in one namespace or adapter:
1(ns acme.discovery.consul
2 (:require [clojure.data.json :as json])
3 (:import [java.net URI]
4 [java.net.http HttpClient HttpRequest HttpResponse]
5 [java.time Duration]))
6
7(def ^HttpClient client
8 (-> (HttpClient/newBuilder)
9 (.connectTimeout (Duration/ofSeconds 2))
10 (.build)))
11
12(defn service-url [consul-base service-name]
13 (let [request (-> (HttpRequest/newBuilder
14 (URI/create (str consul-base "/v1/catalog/service/" service-name)))
15 (.timeout (Duration/ofSeconds 2))
16 (.GET)
17 (.build))
18 response (.send client request (HttpResponse$BodyHandlers/ofString))
19 service (first (json/read-str (.body response) :key-fn keyword))]
20 (when service
21 (str "http://" (:ServiceAddress service) ":" (:ServicePort service)))))
The important design move is not the specific registry call. It is the decision to keep registry dependence localized and replaceable.
Service discovery answers “where is the service?” Coordination answers harder shared-state questions such as:
Tools such as ZooKeeper, etcd, or Consul can help with coordination. ZooKeeper remains historically important and still exists in inherited platforms, especially around older messaging or coordination-heavy systems. But it should not be taught as the default answer for new service discovery in platform-managed applications.
That distinction matters because discovery and coordination fail differently:
Whatever discovery mechanism you choose, your Clojure code still has to survive:
That means the service client needs:
Discovery tells you where to call. It does not guarantee the dependency is ready to help.
flowchart LR
A["Clojure Service"] --> B["Platform Discovery Layer"]
B --> C["Kubernetes Service DNS"]
B --> D["Consul Catalog or DNS"]
B --> E["Gateway / Load Balancer"]
A --> F["Retry, Timeout, and Circuit Policies"]
A --> G["Logs, Metrics, and Traces"]
The important thing to notice is that discovery is only one layer. Operational resilience sits beside it, not beneath it.
Distributed systems discussions often slide straight from discovery into consensus algorithms. That is too big a jump. The practical question for most application teams is simpler:
If the workflow only needs to find a healthy stateless dependency, DNS or a service registry is often enough. If the workflow depends on exclusive ownership or ordered mutation, you may need a real coordination story.