Sync vs Async & When to Go Event-Driven
Async decouples in time, space, and synchronization at the cost of eventual consistency; keep the result-bearing or consistency-sensitive steps synchronous and make independent side effects events.
The chain of availability
A synchronous call blocks the caller until the callee returns. The caller's latency is the sum of every hop, and the caller's availability is the product of every downstream's availability. Chain five services at 99.9 percent each synchronously and your effective availability is about 99.5 percent, because any one being down fails the whole request. Async breaks that chain by putting a broker between producer and consumer.
Async buys you three specific decouplings, and naming them is how you sound senior:
- Time decoupling (buffering). The producer writes to the broker and moves on; the consumer processes later, at its own pace. A traffic spike that would overwhelm a synchronous downstream instead grows a queue that drains when load falls.
- Space decoupling (location). The producer does not know or care which service, or how many of them, consume the message. You add a fraud checker or a new analytics consumer without touching the producer.
- Synchronization decoupling (non-blocking). The producer does not wait for the work to finish. Its latency drops to the cost of one durable write to the broker (single-digit ms) instead of the sum of all downstream work.
The price is eventual consistency. The moment you make a step async, the effect (inventory decremented, email sent) happens after the response, so a follow-up read can observe a state where the effect has not landed yet. You must design for that gap.
Command versus event
A command ("ChargeCard") is an instruction to one specific handler that must succeed and often returns a result, so it tends to stay synchronous. An event ("OrderPlaced") is an immutable statement of fact that already happened; any number of consumers react to it, and the producer does not care what they do. Events point the coupling arrow away from the producer, which is why they scale fan-out cleanly.
The failure mode also inverts. Synchronously, a failure is an error returned to the caller who can retry right now. Async pushes failure into the background: a consumer that throws must be retried with backoff, and after N failures the message lands in a dead-letter queue (DLQ). You now need retry policy, idempotent consumers (because retries duplicate), a DLQ, and monitoring on consumer lag and DLQ depth. The user already got a 200; nobody is watching the screen when the async step breaks.
Interview nuance: the wrong turn is adding a broker to simple CRUD that needs a strong-consistency read right after the write. If a user saves a setting and immediately reads it back, an async write behind a queue gives them a stale read and a support ticket. Async pays off when the follow-up work is genuinely independent of the response.
Sync: client -> [payment] -> [inventory] -> [email] -> [analytics] -> 200
latency = sum of all; one down = request fails
Async: client -> [payment] -> 200
|
OrderPlaced --> broker --> [inventory]
--> [email]
--> [analytics]
Recap: async decouples in time, space, and synchronization, trading immediate consistency for throughput and availability; keep steps synchronous when the caller needs the result or a consistent read, and make independent side effects events.
Checkout involves several steps. Sort each one: keep it synchronous in the request, or make it an async event behind the broker.
Apply
Your turn
The task this lesson builds to.
Design the checkout flow for an e-commerce site: decide which steps stay synchronous (payment auth) and which become async events (inventory, email, analytics), and justify each boundary.
Think about
- What are the three decouplings async buys (time, space, synchronization)?
- Which steps need synchronous consistency, and which tolerate eventual?
- How does the failure mode shift from sync errors to background retries/DLQ?
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Design the async boundary for Uber's ride-request flow at the moment a rider taps 'Confirm,' where the dispatch match must feel near-instant (sub-second) but a single ride fans out to pricing finalization, driver notification, ETA updates, fraud scoring, and the trip-history/analytics pipeline. Decide what stays on the synchronous request path and what becomes events.
Think about
- What is the narrow synchronous path the rider is actually waiting on?
- Why must the driver reservation be strongly consistent?
- How do you keep a lagging analytics consumer from degrading dispatch?