two ways for services to talk

Once a system is split into services (see Monolithic vs. Microservices Architecture), every inter-service call has to pick one of two communication styles.
StyleHow it works
SynchronousThe caller sends a request and blocks, waiting for a response before proceeding.
AsynchronousThe caller fires a message and moves on immediately, without waiting for the receiver to process it.

a purely synchronous checkout, and where it breaks

Trace an order through PlateRoute (see the previous page) with every call synchronous: Order Service calls Restaurant Service to confirm the item is still on the menu and waits. On success, it calls Payment Service and waits. On success, it calls Dispatch Service to find a driver and waits. Only after all three succeed does it respond to the customer.
This is easy to reason about — the code reads top to bottom, like a single function — but it has real costs:
  • High latency: the customer waits for the sum of every downstream call, not the slowest one.
  • Tight coupling: if Dispatch Service is slow or down, checkout is down too, even though the payment already succeeded.
  • Cascading failure handling: a failure partway through (say, payment succeeds but dispatch fails) means the Order Service has to know how to undo the payment — compensating logic that gets complicated fast.

going purely asynchronous instead

The opposite extreme: Order Service fires off requests to Restaurant, Payment, and Dispatch services and doesn't wait for any of them. This removes the latency and coupling problems above — but introduces a new one. If the Payment call was fire-and-forget and it silently fails, the order might get marked "placed" and dispatched anyway, with no payment collected.
The lesson isn't "async is better" — it's that some steps in a flow are genuinely mandatory (payment must succeed before dispatching food) and some are genuinely best-effort (sending a "your order is on the way" push notification can fail silently without breaking the order).

the hybrid approach most real systems use

The practical pattern: use synchronous calls for steps that must succeed before the next step can safely happen, and asynchronous messaging for everything that's best-effort or can be retried independently.
For PlateRoute: Order Service calls Payment Service synchronously, because dispatching a driver before payment clears is a real business risk. Once payment succeeds, it responds to the customer immediately and publishes an event ("order placed") asynchronously. Dispatch Service and Notification Service both subscribe to that event and act on their own schedule — if Notification Service is temporarily down, the order still gets delivered; the push notification just arrives late.

why a message queue, not just "async", is the real fix

Naive fire-and-forget async has its own failure mode: if the receiving service is down at the exact moment the message is sent, the message is simply lost. A message queue sits between the sender and receiver, persisting messages until a (possibly temporarily unavailable) subscriber comes back and consumes them — covered in depth in Distributed Messaging Queues. This is what actually makes the async half of the hybrid approach safe to rely on, rather than just faster.
Benefit of routing async calls through a queueWhy it matters
No lost events on downtimeA subscriber that's redeploying or crashed doesn't lose the messages sent while it was down.
Independent scalingDispatch and Notification services can each scale to their own load, without Order Service knowing or caring.
Simpler error handlingOrder Service no longer needs bespoke rollback logic for every downstream failure — it only has one synchronous dependency (payment) to handle carefully.

related topics

reference