Service Communication
Synchronous REST/gRPC vs asynchronous events, with the same Orders-to-Inventory example built both ways.
Two ways services talk
Once OrdersService and InventoryService are separate processes, every interaction between them has to cross the network, and you have exactly two styles to choose from:
| Synchronous (REST/gRPC) | Asynchronous (events/queues) | |
|---|---|---|
| Caller waits for a response? | Yes, inline | No — fires and moves on |
| Coupling | Temporal — callee must be up and reasonably fast | Only through the event's schema |
| Failure mode | Caller fails or blocks if callee is down/slow | Message waits in the broker until a consumer is ready |
| Adding a new consumer | Requires changing the caller | Zero changes to the producer |
| Debugging one request | Straightforward — one call stack | Harder — spread across a broker and multiple consumers |
Below is the same use case built both ways: an OrdersService telling InventoryService to reserve stock for a newly placed order.
Synchronous: a direct REST call
Orders calls Inventory's HTTP API and waits for the answer as part of handling the checkout request itself:
// Inside OrdersService — calling InventoryService synchronously over HTTP
HttpClient client = HttpClient.newHttpClient();
String requestBody = """
{"orderId":"order-482","items":[{"sku":"SKU-100","qty":2}]}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://inventory-service/api/reservations"))
.header("Content-Type", "application/json")
.header("X-Request-Id", currentRequestId()) // propagate for tracing — see Observability
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 409) {
throw new OutOfStockException("order-482");
}
// 200 OK here means the reservation is confirmed before OrdersService responds to its own caller
OrdersService cannot finish creating the order until InventoryService answers. If Inventory is slow or temporarily down, checkout is slow or fails right along with it — which is exactly why a call like this should be wrapped in a circuit breaker (System Design track) rather than retried blindly.
gRPC is a synchronous alternative worth knowing about: it uses HTTP/2 and Protocol Buffers instead of JSON-over-HTTP/1.1, giving you a strongly-typed, code-generated contract and lower serialization overhead. The choice between REST and gRPC is about payload format and contract strictness, not about sync vs. async — both are still the caller blocking on an answer.
Asynchronous: publishing an event
Orders publishes a fact about what happened, and doesn't wait for (or know about) anyone reacting to it:
// Inside OrdersService — publishing an event instead of calling Inventory directly
KafkaTemplate<String, String> kafkaTemplate = ...;
String event = """
{"eventType":"OrderPlaced","orderId":"order-482","items":[{"sku":"SKU-100","qty":2}]}
""";
kafkaTemplate.send("order-events", "order-482", event);
// OrdersService returns success to its own caller right here — it never waits on Inventory
// Inside InventoryService — reacting to the event whenever it gets around to it
@KafkaListener(topics = "order-events", groupId = "inventory-service")
public void onOrderPlaced(String rawEvent) {
OrderPlacedEvent event = parse(rawEvent);
try {
reserveStock(event.orderId(), event.items());
} catch (OutOfStockException e) {
// no caller to return a 409 to — publish a compensating event instead
kafkaTemplate.send("order-events", event.orderId(),
toJson(new StockReservationFailed(event.orderId())));
}
}
Notice what's missing compared to the REST version: there's no if (response.statusCode() == 409) anywhere in OrdersService. It has already told the customer their order was placed by the time Inventory even looks at the message. If stock genuinely isn't available, the only way to communicate that back is another event — this is the Saga pattern's compensating action, not a return value.
When to choose which
- Need an answer before you can respond to your own caller (is this in stock? is this payment valid?) — use a synchronous call.
- The rest of the system just needs to know something happened, and may have several independent, unrelated reactions to it (send a receipt email, update analytics, reserve stock) — publish an event.
- Real systems mix both in the same flow: validate synchronously what must be checked before responding, then publish an event for everything that can happen afterward.
Common mistakes
- Making every inter-service call synchronous "because it's simpler" — this produces a fragile, tightly-coupled call graph that is, in practice, less reliable than the monolith it replaced.
- Firing an event and assuming it's already been handled by the time the next line of code runs — the broker accepting a message only means it was accepted for delivery, not that any consumer has processed it yet.
- Forgetting to propagate a request/correlation ID across both styles of call — as an HTTP header for sync calls, as a field on the event payload for async ones — without it, tracing a single request across services becomes guesswork (see Observability for Microservices).
- Choosing async purely to "decouple" a call that genuinely needs an inline answer, then bolting a synchronous-feeling polling loop on top of it to fake one.