Skip to content
QSWEQB
mediumDesignConceptMidSeniorStaff

Service A needs something from service B. When should that be a synchronous call and when should it be an event?

A synchronous call couples availability and latency but keeps the outcome knowable at the point of decision; an event decouples both but converts every failure into something the caller will never learn about. Choose by asking whether the caller needs the answer to proceed, not by which style is fashionable.

3 min readUpdated 2026-07-26

What the interviewer is scoring

  • Does the candidate distinguish temporal coupling from the domain question of whether the answer is needed now
  • Whether they can state what happens to an event whose consumer is broken, and who notices
  • That availability multiplies across a synchronous chain and they can do the arithmetic
  • Whether they raise the ordering and idempotency obligations an event takes on
  • Does the candidate name a case where asynchronous is the wrong answer rather than defending it universally

Answer

The question underneath the question

The useful test is not "which is more scalable". It is whether the caller can make its next decision without the answer. If a checkout cannot decide whether to show a confirmation page until the payment authorises, that is a synchronous dependency and dressing it up as an event only moves the waiting somewhere less visible. If a checkout wants a receipt emailed, the caller does not need to know the outcome to proceed, and holding the request open while an SMTP server is slow is a self-inflicted wound.

Most integration arguments go wrong because both sides are arguing about the mechanism when the disagreement is really about that question. Answer it first and the mechanism usually follows.

What a synchronous call actually couples

Three things, and they are worth separating because teams often accept all three when they only meant to accept one.

It couples availability: B being down means A is down for that operation. It couples latency: A's p99 now contains B's p99, and tail latencies compose badly. It couples capacity: a traffic spike on A becomes a traffic spike on B whether or not B was provisioned for it.

The availability arithmetic is what candidates most often skip. Independent dependencies multiply, so a chain of four services each at 99.9% gives roughly 99.6% for the composite — from about 43 minutes of monthly downtime to nearly three hours. That is a real result and it is why a deep synchronous call graph degrades faster than any single service in it. The mitigation is not to make each service better; it is to have fewer of them on the critical path.

What an event couples instead

An event removes temporal coupling and replaces it with a set of obligations that are easy to underestimate.

The consumer must tolerate duplicates, because every practical broker delivers at least once and a redelivery after a partial failure is normal operation rather than an edge case. It must tolerate reordering, unless you have partitioned by a key that guarantees order for the entities that need it, which constrains your partitioning strategy for the life of the topic. And someone must own the poison message: an event that fails every attempt, lands in a dead-letter queue, and sits there until a human notices.

That last one is the honest cost. In a synchronous design, a failure is returned to a caller who is still on the phone and can react. In an asynchronous design, a failure happens in a process nobody is watching, and the business consequence — an order that never shipped, a refund that never issued — surfaces days later through a customer complaint. Asynchronous integration does not remove failure. It converts loud, immediate failure into quiet, deferred failure, and the price of admission is the monitoring that makes it loud again.

Where the boundary usually lands

A pattern that holds up: queries synchronous, state changes asynchronous, and commands whichever the caller's decision requires. Reading a customer's credit limit to decide whether to accept an order is a query and wants an answer now. Telling six downstream systems that an order was accepted is a fact that has already happened, and each consumer's failure to process it should not retroactively fail the order.

The awkward middle is a command that changes state and whose outcome the caller needs. Payment authorisation is the canonical one. The usual resolution is to stay synchronous but bound the damage — a timeout shorter than the caller's own budget, a circuit breaker so a slow dependency stops consuming caller threads, and a defined behaviour on failure that is a product decision rather than a stack trace.

The failure candidates rarely mention

A synchronous call with a timeout has three outcomes, not two: success, failure, and unknown. When the timeout fires you do not know whether B processed the request. If A retries, and the operation is not idempotent, you have double-charged someone.

This is why an idempotency key on the write is not an optional refinement — it is what makes the retry safe, and without it the timeout you added for resilience has quietly created a correctness bug. Candidates who introduce timeouts and retries without addressing the unknown outcome have made the system less reliable while believing they made it more so.

Asynchronous does not mean the failure went away. It means nobody is standing there when it happens, so you have to build the thing that notices.

Likely follow-ups

  • Your synchronous chain is four services deep, each at three nines. What is the end-to-end availability?
  • The consumer of your event has been down for two hours. What does recovery look like?
  • How would you let a caller stay synchronous while the work behind it is asynchronous?
  • Where does a request-reply-over-a-queue pattern sit in this framing, and what does it actually buy?

Related questions

Further reading

integrationcouplingevent-drivenavailabilityapi-design