← Back to the section

When there is more than one service, they have to talk somehow. The most obvious way: one service calls another over HTTP and waits for a reply. But there is another way too: a service publishes a message to a queue and moves on, without waiting for anyone.

Let's break down what each approach means, what the real difference is, and how not to pick the wrong one.

Synchronous call: you ask, then you wait for the answer

Imagine you call a delivery service and stay on the line while the operator checks whether your address is serviceable. Until the operator answers, you can't do anything else.

That is exactly how a synchronous call between services works. Service A sends an HTTP request to service B and blocks, waiting for the reply. Only after it gets the result does it continue.

Service A  ──── HTTP GET /users/42 ────►  Service B
           ◄─── { "name": "Ivan" } ──────

This is familiar and simple: you can see what's happening, errors come back immediately, the result is here and now.

But there is a price: both services must be running at the same time. If service B is down, the operation in service A fails too. If B is slow, A is slow too. Their fates are tied together.

Asynchronous call: you publish, then you live on

Now a different scenario. Instead of calling the operator, you leave a message on the answering machine: "Place order #42." And you immediately go do other things — the operator will call back once they've handled it.

That is how communication through a message broker (Kafka, RabbitMQ and the like) works. Service A publishes an event to a queue and does not wait: it has already finished its work. Service B reads that event whenever it is ready.

Service A  ──── OrderPaid ────►  [ queue ]  ────►  Service B
           (continues its work immediately)

If service B is temporarily down, the event sits in the queue and doesn't go anywhere. When B comes back up, it will process it.

The flip side: there is no reply. A doesn't know when B will process the event, or whether it will process it at all. The result will appear in seconds, or even later.

The main question when choosing

Ask yourself one question: do you need the neighboring service's result right now in order to continue the operation?

If yes — a synchronous call. Examples:

  • check the account balance before a debit;
  • get the shipping cost to show the total in the cart;
  • authorize a payment in the payment gateway.

If no — an asynchronous event. Examples:

  • notify the user by email after an order is placed;
  • update the search index when a product changes;
  • award bonus points;
  • push data into analytics.

A good test: what screen will the user see if the neighbor didn't answer? If it's "the operation didn't complete" — you need a synchronous call. If it's "the same as usual" — an asynchronous event will do.

What happens when the neighbor is unavailable

This is the most important practical difference.

With a synchronous call, the neighbor being down is your failure. If the payment gateway is down, the payment doesn't go through. That's honest: without the gateway the payment is meaningless anyway.

With asynchronous communication, the neighbor can be down — that is not your problem. The order is placed successfully, the event goes into the queue, and the notification service will process it later, once it's back up.

A common mistake: tying the main scenario to a secondary service with a synchronous call. If placing an order fails because the bonus-points service is down — that's bad architecture. Bonus points are secondary; they should be asynchronous.

One recipient or several

With a synchronous call you have one addressee: service A knows and calls a specific service B.

When you publish an event, there can be as many recipients as you like. The "order paid" event is simultaneously of interest to the warehouse, the notification service, the analytics system, and the loyalty department. They all subscribe to that event and process it independently.

The key point: the event's publisher does not know who reads it. When a new consumer appears, the order service doesn't need to be touched — the new service simply subscribes to the queue.

With synchronous calls, adding a new consumer requires changes in the calling code.

Load and buffering

A synchronous call passes the load straight through. If service A gets 10,000 requests per second, the same number of requests will hit service B. Its limits become your limits.

A message broker works as a buffer. Service A publishes events quickly, service B reads them at its own pace. Peak loads are smoothed out naturally.

Common mistakes

A chain of synchronous calls. Placing an order sequentially calls the warehouse, billing, delivery, and notifications. If each service is available 99% of the time, a chain of five steps is already only 95% available. Latencies add up. A failure of the fourth service undoes the work of the previous three. Such multi-step scenarios naturally fit onto events, not onto a chain of calls.

A command in the queue. A "SendEmailRequested" event is a disguised command to a specific service, not a domain fact. This is the worst of both worlds: the coupling of a command plus the complexity of asynchrony. Events name things that have already happened: "OrderPaid", "UserRegistered", "InvoiceIssued".

Asynchrony where the user is waiting for an answer. "Your request has been accepted for processing" instead of the payment result — and the user is forced to refresh the page and check the status. Interactive steps, where a person is looking at the screen and waiting, are synchronous by nature.

Events without delivery guarantees. Publishing an event to the broker after committing the transaction with a plain call: if the application crashes between the commit and the send, the event is lost and the data drifts apart. The solution is the outbox pattern: the event is first saved into the same database within the same transaction, and a separate process sends it.

A broker for the broker's sake. If between two services of the same team there is one consumer and a steady load, adding Kafka for the sake of "proper architecture" is not worth it. REST with retries is easier to debug and observe.

How they combine

In a single system both approaches are used at the same time — and that's fine.

Synchronous calls where the user is waiting for an interactive answer. Asynchronous events where domains tell each other about facts that have happened.

The extremes are suspicious. A system built only on HTTP calls is a set of coupled services that fall over together. A system built only on events, including payment and authorization scenarios, means users staring at an endless spinner.

In short

  • Synchronous call: A calls B and waits for a reply. Both must be running at the same time.
  • Asynchronous event: A publishes a fact to a queue and moves on. B will process it when ready.
  • The main criterion: do you need the neighbor's result right now?
  • Synchronously: check the balance, authorize a payment, compute the cart total.
  • Asynchronously: notifications, analytics, bonus points, index updates.
  • With a synchronous call, the neighbor being unavailable is your failure. With an event, it's the neighbor's problem — it will catch up later.
  • Events are for domain facts, not for commands to a specific service.
  • Events without an outbox are lost if there's a failure between writing to the database and sending to the broker.
  • Both approaches combine in one system — chosen by the task, not by principle.
  • Distributed patterns — saga, outbox, idempotency: the toolkit of the asynchronous side.
  • Resilience patterns — timeouts, retries, circuit breaker: the toolkit of the synchronous side.
  • AMQP vs Kafka — the next choice, if you've decided to use events.