Messagingsynchronousasynchronousrequest/responseevent-driventimeouts

Request/Response vs Event-Driven

Call synchronously when the caller needs the answer now, the chain is short and consistency must be immediate; go asynchronous when the work can happen later, fan out, or arrive in bursts — and use the hybrid, a synchronous command with asynchronous consequences, for most real user-facing writes.

▶ InteractiveInterview questionDebug it
Progress
What problem does this solve?

Teams either make everything synchronous — and checkout p99 becomes the sum of five services, one of which is having a bad day — or make everything asynchronous and cannot tell the user whether their order exists. The decision has concrete criteria: who needs the result, when, and what happens if the consumer is slow.

The same call, two shapes

Synchronous: the client calls Order, which calls Inventory and Payment in turn, waits for both, and returns 201 with the confirmed order. The client knows the outcome; the state is consistent the moment the response arrives; and the request path’s latency and availability are the product of every hop — three services at 99.9% each give 99.7%, and p99 stacks. Asynchronous: Order writes the row, publishes OrderCreated, and returns 202 in 20 ms; Inventory and Payment consume the event later. The client gets a fast answer that says "accepted", not "done", and for a while the order exists in a state the user may see as pending.

Neither shape is more "scalable" in the abstract. The synchronous one is simpler to reason about and to debug — one trace, one error — and fine when the chain is two hops with tight timeouts. The asynchronous one isolates failure and absorbs bursts, and requires everything Message Queues and Event-Driven Architecture describe: idempotent consumers, lag monitoring, status the user can query.

Synchronous chain versus event fan-out for the same order
POST /orderssync: reserve (waits)sync: charge (waits)async: publish, return 202ClientOrder ServiceInventoryPaymentBus · OrderCreatedInventory consumerPayment consumer
ClientGateway / LBServiceWorkerDatabaseCacheQueue / LogObject storageCDNExternal system

When each one is right

Use synchronous when the caller needs the result to continue (card authorisation before showing "paid"), when the dependency chain is short and owned by you, and when the caller must observe a consistent state right after the call (an admin toggling a feature flag and reloading). Use asynchronous when the work can happen later without the user waiting (emails, indexing, analytics), when one fact has many independent reactions, when producers are bursty and consumers steady, or when the consumer is unreliable and its failures must not become yours.

The matrix is the decision; the finder sync-or-async walks the same questions. The last row is the one most often forgotten: an asynchronous design needs a way to tell the user what happened, which means job status, notifications, or a read model that shows "pending" honestly.

Choosing between a synchronous call and an asynchronous event
QuestionSynchronous callAsynchronous event
Does the caller need the result now?Yes — it is part of the responseNo — "accepted" is enough
Dependency chainShort (1–2 hops), owned by youAny length; each consumer independent
Consistency after the callImmediate: the response reflects the stateEventual: consumers catch up later
CouplingTemporal + availability + latencySchema only
Failure surfaceTimeouts, cascading latency, retry amplificationBacklog, stale state, duplicates, lost events
Burst handlingOverload → 503s and timeouts nowQueue absorbs; drains later
Fan-outCaller must call each targetFree: subscribe another consumer
DebuggingOne trace, one errorCorrelation ids across consumers, lag dashboards
Telling the user the outcomeThe responseStatus endpoint, push, or a read model with "pending"

How each fails, and the hybrid

Synchronous chains fail loudly and together. A slow Inventory raises Order’s latency; Order’s callers time out and retry; three retries at three layers become 27 calls per user action, and the slow service is finished off by its own clients. The defences are timeouts shorter than the caller’s, retry budgets, and a Circuit Breaker — see Reliability Patterns. Asynchronous systems fail quietly and later: the producer is green, the consumer is down or slow, and the backlog grows for hours until a customer asks why the confirmation never came. Stale read models show yesterday’s inventory; a duplicate delivery reserves stock twice; an event lost between commit and publish never reaches anyone. The defences are consumer-lag alerts, idempotent consumers, and the outbox pattern.

Most user-facing writes want the hybrid: synchronous for the command, asynchronous for the consequences. POST /orders validates, reserves stock and authorises the card synchronously — those decide whether the order exists — and returns 201. Everything downstream of that fact (email, analytics, warehouse, recommendation refresh) is an event. The user gets an immediate, truthful answer; the fan-out cannot slow or fail checkout; and the boundary is principled: what must be decided now is a call; what merely follows is an event.

Sync command, async consequences — the event is written in the same transaction as the order (outbox)
1async function createOrder(cmd: CreateOrder): Promise<OrderView> {
2 const auth = await payments.authorize(cmd.card, cmd.total, { timeoutMs: 2000 }) // decide now
3 if (!auth.ok) throw new PaymentDeclined(auth.reason)
4
5 return db.transaction(async (tx) => {
6 const order = await tx.insert('orders', { ...cmd, status: 'confirmed', authId: auth.id })
7 await tx.insert('outbox', { topic: 'orders', key: order.id, type: 'OrderCreated', payload: order })
8 return toView(order) // email, analytics, warehouse react later via the relay
9 })
10}

Key points

  • Synchronous when the caller needs the result, the chain is short, and state must be consistent on return.
  • Asynchronous when work can happen later, when a fact has many reactions, or when bursts must be absorbed.
  • Sync fails loudly and together (timeouts, cascading latency, retry amplification); async fails quietly and later (backlog, stale state, duplicates).
  • Hybrid: what must be decided now is a call; what merely follows is an event.
  • An async design owes the user a status: pending states, a job endpoint, or a notification.

The same call, synchronous and asynchronous

The same call, synchronous and asynchronous
“Place order → send confirmation.” Tune B and compare what the user experiences on each path.
HTTP, waitsenqueuelaterA: Orders (sync)A: Orders (async)B: NotificationsQueueB: Notifications
Highlight path
sync latency
330 ms
async latency
34 ms
sync availability
94.81%
async availability
99.89%
A → BA → queue → B
user-perceived latency330 ms34 ms
on B failure the user sees500 — "try again" (and maybe a double order)202 Accepted — confirmation arrives later
the work islost unless the user retriesqueued; delivered when B is healthy
availability of "place order"94.81% = A × B99.89% = A × queue
couplingtemporal: B must be up right nowB may be down, slow, or replaced
Sending a confirmation email does not need to finish before the order is accepted. Synchronous, the user waits 300 ms extra and inherits B's 5% failure rate for no reason; asynchronous, the order is accepted in 34 ms and the email is a consequence. The rule: when the caller needs the answer now, go sync; when it does not, sync couples availabilities for no reason. Toggle “caller needs B's answer now” to see the other case.

How data moves through it

One request or event, hop by hop.

  1. 1Client → Order Service: POST /orders with an idempotency key.
  2. 2Order Service → Payment API: synchronous authorise with a 2 s timeout; declined → 402 returned immediately.
  3. 3Order Service → Orders DB: insert order and outbox row in one transaction; return 201 with the order view.
  4. 4Outbox relay → Bus: publish OrderCreated keyed by orderId.
  5. 5Bus → Email / Analytics / Warehouse: consume independently; each records the event id before acting.
  6. 6Client → Order Service: GET /orders/{id} shows confirmed, with shipment status filled in as consumers catch up.

When to use — and when not

Use it when
  • Choose synchronous for authorisation, validation and any check whose result changes the response.
  • Choose asynchronous for notifications, indexing, analytics, exports, and third-party webhooks.
  • Choose the hybrid for user-facing writes with downstream side effects — orders, sign-ups, uploads.
  • Choose asynchronous when a consumer is another team’s system whose uptime you do not control.
Avoid it when
  • Do not make a decision asynchronous: "reserve stock via event and hope" produces oversold inventory.
  • Do not chain five synchronous services for one request; the availability product and stacked p99 will show up first.
  • Do not go asynchronous to hide a capacity problem; the backlog surfaces it later and larger (Backpressure).
  • Do not go asynchronous without a way for the user or operator to see outcome and lag.

Tradeoffs

Complexity
low → high
Ops cost
low → high
Latency
low → high
Consistency
weak → strong
Scalability
poor → strong

Ratings describe the hybrid: fast, consistent on the decision path, eventually consistent downstream, with the operational cost of a broker and lag monitoring for the consequence side.

How it fails

  • Synchronous: a 30 s gateway timeout plus a client retry with no idempotency key charges the customer twice — challenge double-charge-after-timeout.
  • Synchronous: retry amplification (3 × 3 × 3 = 27) turns one slow dependency into a full outage — challenge retry-storm-took-down-payments.
  • Asynchronous: the backlog grows for hours with every service healthy; oldest-message age is the only metric that would have shown it.
  • Asynchronous: a read model lags and the UI reads it right after the command, showing the user their order does not exist — challenge read-model-shows-stale-order.
  • Asynchronous: publish after commit without an outbox; the process dies in between and the event is lost forever.

How it scales

  • Synchronous paths scale by adding instances behind an LB but inherit the slowest dependency’s capacity.
  • Asynchronous paths scale consumers independently of producers, bounded by partitions and the downstream store.
  • The hybrid keeps the decision path minimal — fewer hops to scale — and moves the volume to consumers that can lag safely.
  • As services multiply, push more consequences to events; the synchronous core should stay at two or three hops.

How it interacts with databases, queues, caches, APIs and external systems

  • Database: the outbox table makes "state + event" atomic; the read path may show fields still being filled in by consumers.
  • Queue/bus: carries the consequence side; a task queue for jobs, a log for streams that several teams read.
  • Cache: synchronous reads can be served from cache; consumers invalidate on events rather than by TTL.
  • External APIs: decisions from third parties (payment authorisation) stay synchronous with timeouts; their notifications (webhooks) arrive asynchronously and need idempotency keys.
  • API gateway: enforces per-call timeouts on the synchronous path so the 202/201 contract holds under load.