Communication Between Clojure Microservices

How Clojure microservices choose between HTTP, gRPC, and event streaming, and which communication patterns fit synchronous versus asynchronous boundaries.

Communication between Clojure microservices is not mainly a library choice. It is an architecture choice about coordination, latency, failure handling, and how much coupling a team is willing to accept. The healthy order of thinking is:

  1. decide whether the boundary is synchronous or asynchronous
  2. decide how strong the contract needs to be
  3. decide what operational behavior the system can tolerate
  4. only then pick a library or client runtime

Older pages often go the other way around and start by naming a few Clojure libraries. That tends to produce accidental architecture.

Start With Synchronous Versus Asynchronous

Use synchronous communication when the caller truly needs an immediate answer:

  • fetch current inventory
  • validate a token or policy decision
  • calculate a price before responding
  • ask a small internal service for data needed right now

Use asynchronous communication when the real requirement is durable propagation rather than immediate coordination:

  • publish an order-created event
  • update downstream projections
  • fan out work to background processors
  • integrate systems that should continue even when one consumer is temporarily unavailable

The biggest mistake is to use synchronous calls for work that should have been modeled as delayed propagation, or to use eventing for queries that really need immediate consistency.

HTTP and JSON Are Still The Practical Default

For many service-to-service interactions, plain HTTP with JSON remains the best default because it is:

  • easy to inspect
  • easy to debug
  • broadly interoperable
  • straightforward for internal and external APIs alike

In modern Clojure code, that does not mean you must default to an older synchronous client library. For simple internal clients, plain Java interop with the JDK HTTP client is often enough:

 1(ns myapp.clients.catalog
 2  (:import [java.net URI]
 3           [java.net.http HttpClient HttpRequest HttpResponse$BodyHandlers]))
 4
 5(def client (HttpClient/newHttpClient))
 6
 7(defn fetch-item [id]
 8  (let [request (-> (HttpRequest/newBuilder
 9                     (URI/create (str "http://catalog.internal/items/" id)))
10                    (.header "accept" "application/json")
11                    (.GET)
12                    (.build))
13        response (.send client request (HttpResponse$BodyHandlers/ofString))]
14    {:status (.statusCode response)
15     :body (.body response)}))

That example is intentionally boring. Boring is good when the boundary itself is already easy to reason about.

Libraries such as http-kit still make sense when you want a Clojure-friendly client/server surface with sync and async options. But the page should not teach “use clj-http” as though it were the timeless center of Clojure microservice communication.

Use gRPC When Contracts And Streaming Matter

gRPC is strongest when the boundary benefits from:

  • explicit schema-first contracts
  • generated client/server stubs
  • polyglot interoperability
  • streaming
  • tighter request and response typing than ad hoc JSON payloads

In Clojure, teams often use generated Java classes and stubs through interop instead of searching for a uniquely Clojure-native abstraction.

 1(ns myapp.clients.inventory
 2  (:import [io.grpc ManagedChannelBuilder]
 3           [com.example.inventory InventoryServiceGrpc GetInventoryRequest]))
 4
 5(defn fetch-inventory [sku]
 6  (let [channel (.. (ManagedChannelBuilder/forAddress "inventory.internal" 50051)
 7                    (usePlaintext)
 8                    build)]
 9    (try
10      (let [stub (InventoryServiceGrpc/newBlockingStub channel)
11            request (.. (GetInventoryRequest/newBuilder)
12                        (setSku sku)
13                        build)
14            response (.getInventory stub request)]
15        {:sku (.getSku response)
16         :available (.getAvailableUnits response)})
17      (finally
18        (.shutdown channel)))))

The point is not that every Clojure team should prefer gRPC. The point is that when you do choose gRPC, Java interop is often the most practical path.

Use Event Streaming For Decoupling and Replay

When services should evolve more independently, event streaming is often a better fit than direct request/response. Kafka is a common example because it gives you:

  • durable event logs
  • consumer groups
  • replay
  • looser runtime coupling

In Clojure, using the official Kafka Java client through interop is often clearer than teaching a thin wrapper as if the wrapper were the architecture.

 1(ns myapp.events.orders
 2  (:import [java.util Properties]
 3           [org.apache.kafka.clients.producer KafkaProducer ProducerRecord]))
 4
 5(defn producer-props []
 6  (doto (Properties.)
 7    (.put "bootstrap.servers" "localhost:9092")
 8    (.put "key.serializer" "org.apache.kafka.common.serialization.StringSerializer")
 9    (.put "value.serializer" "org.apache.kafka.common.serialization.StringSerializer")))
10
11(defn make-producer []
12  (KafkaProducer. (producer-props)))
13
14(defn publish-order-created! [producer order-id payload]
15  (.send producer (ProducerRecord. "orders.events" order-id payload)))

This kind of communication trades immediate certainty for autonomy. That is the right trade only when the business process can tolerate it.

Sente Is Usually Not The First Service-To-Service Answer

Sente is valuable for real-time browser/server and interactive channel use cases. It is not usually the first architecture choice for generic microservice-to-microservice communication. That distinction matters because older pages sometimes blur interactive application messaging and backend integration messaging into one category.

If the main boundary is between browser clients and a live application channel, Sente may be a strong fit. If the main boundary is between services coordinating domain work, HTTP, gRPC, and event streaming are more often the central decision set.

Compare Protocols By System Behavior

Protocol or PatternBest UseMain StrengthMain Trade-Off
HTTP/JSONImmediate request/response APIsSimplicity and debuggabilityTighter runtime coupling
gRPCStrong contracts, polyglot, streamingTyped schemas and efficient transportMore tooling and contract overhead
Kafka or event streamingDecoupled propagation and replayLoose coupling and durable eventsEventual consistency and harder debugging
WebSocket-style channelsLive interactive sessionsBidirectional near-real-time updatesNot the general default for backend integration

The right question is not “which one is modern?” The right question is “which failure mode and consistency model does this boundary need?”

Reliability Rules Matter More Than Syntax

No matter which protocol you pick, service communication needs a few disciplined rules:

  • set timeouts and deadlines
  • use retries sparingly and with jittered backoff
  • design for idempotency
  • propagate correlation IDs
  • keep payload contracts explicit
  • prefer outbox or similarly reliable publication patterns when emitting events after state changes

Without those rules, the code can look elegant while the system behaves badly under pressure.

    flowchart LR
	    A["Caller Service"] -->|HTTP or gRPC| B["Immediate Response Needed"]
	    A -->|Event Publish| C["Broker or Log"]
	    C --> D["Independent Consumer Service"]

The key thing to notice is that these are different coordination models, not just different wire formats.

Key Takeaways

  • Start with coordination needs, not with a favorite library.
  • HTTP/JSON remains the practical default for many service boundaries.
  • gRPC is strongest when schema discipline and streaming matter.
  • Event streaming is a better fit when services should decouple in time and failure handling.
  • Sente is more relevant for interactive realtime channels than for generic backend service meshes.

References and Further Reading

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026