When APIs, when events
I've seen the "APIs versus events" debate in just about every team — and it rarely goes well. People argue from what's modern, what we've already bought, or how it's always been done. I use a few questions about the process itself instead. They usually settle it on their own; this note sums them up, with examples.
Who needs the answer, and when
The question of which architecture is "better" can't be settled. It's more useful to ask who needs the answer, and when: when the caller can't continue without the result, you need a synchronous call; when the result matters "whenever it's ready", or matters to several consumers, you're in event territory. Most disputes end the moment people stop talking about technology and start talking about the process.
When a synchronous API
A synchronous call is the right choice when:
- The caller can't proceed without the answer. Checking a limit before confirming a deal, computing a price in a quote, verifying permissions. The waiting simply belongs to the operation.
- You want errors handled right away, in context. A synchronous call returns the error to whoever asked, the moment they asked — and they can react right away: fix the input, try again, tell the user.
- The flow should be readable. A chain of synchronous calls reads top to bottom and you see the order of steps at a glance. For processes where order and clarity matter, that's worth a lot.
The price you pay: coupled availability. Every link of a synchronous chain inherits everyone else's outages. Ten services at 99.9% availability in a row come out to 0.999¹⁰ ≈ 99.0% — nine hours of downtime a year just became eighty-seven. And the slowest link sets the pace for everyone.
And then there's the retry storm: a wave of repeated calls blows a small outage up into a big one. That's exactly what happened in the DynamoDB outage of September 2015 — the network recovered in moments, but the wave of synchronous retries kept the metadata service overloaded for almost five hours, and the cascade took SQS, CloudWatch and autoscaling with it. So it wasn't the network that decided how long the outage lasted — it was the architecture around it. And in October 2025 the same thing happened again in the same region, this time for fourteen hours.
When events
An event is the right choice when:
- More consumers care about the result. A closed policy matters to commissions, reporting, documents and marketing alike. Publishing one event beats calling four APIs — and a fifth consumer joins next time without you touching the source.
- The producer shouldn't know its consumers. An event says "this happened", not "do this". The source system doesn't have to change every time someone new wants the result. That's the main thing about the event-driven approach: system lifecycles come apart.
- Load spikes shouldn't pass through. A queue between producer and consumer spreads the surge over time instead of letting it cascade into the systems downstream.
- The process tolerates asynchrony. "Within a few seconds" is fine for most downstream steps — the business just has to know it and sign off on it.
The price this time is operational and mental: events are harder to debug. You don't see the process flow in a single trace, consistency is eventual, duplicate deliveries and dead letters (DLQ) show up. Without proper monitoring and idempotent consumers, the "modern architecture" turns into a system nobody understands.
One thing about duplicates that's better known in advance: at-least-once delivery is part of the contract. SQS and Stripe webhooks both say it plainly in their docs — every now and then the same message simply arrives twice. A consumer that answers a second delivery with a second commission payout has a design flaw; duplicates were to be expected. The fix is old and proven: an idempotency key — the same request with the same key runs once, no matter how many times it arrives.
The grey zone and hybrids
The real world rarely fits cleanly into one box:
- Request-reply over events — the caller posts a request to a queue and waits for a reply event. Your spikes get spread over time, but you pay with more complex correlation. Makes sense for long-running operations, not as a substitute for an ordinary call.
- Outbox pattern — when you need to write to your own database and reliably publish an event, write both in one transaction (the event into an outbox table) and leave publishing to a separate process. Solves the classic "written but never sent" problem.
- Synchronous reads, asynchronous writes — a common and healthy compromise: you ask for current state via API, changes get broadcast as events.
The price of each choice
Neither option is free — they differ only in where and when you pay:
| Aspect | Synchronous API | Events |
|---|---|---|
| Answer for the caller | immediate | none / later |
| Coupling between systems | tight (availability, versions) | loose (event contract only) |
| Behaviour under load spikes | forwards the load | spreads it via the queue |
| Error handling | immediate, in call context | delayed, out of context |
| Flow readability | high | needs tooling |
| Operational demands | lower | higher (monitoring, DLQ, idempotency) |
Conclusion
The whole framework fits into three questions: who needs the answer and when, how many consumers there are, and what should happen during an outage. The answer to "API or event?" usually falls out on its own. And when two parts of a process each ask for something different, that's fine — they simply need different things.