Event-driven architecture is presented as the mature alternative to synchronous calls between services. Sometimes it is. It is also a reliable way to convert a debuggable stack trace into a distributed mystery, and to trade a clear failure for a silent one. The decision deserves more nuance than it usually gets.
01What events genuinely buy you
The real benefit is temporal decoupling. The producer does not wait for the consumer, does not need it to be available, and does not need to know it exists. When an order is placed and six downstream systems care - inventory, billing, fulfilment, analytics, notifications, fraud - synchronous calls to all six mean the order fails if any one is down, and takes as long as the slowest.
The second benefit is adding consumers without touching the producer. A new analytics pipeline subscribes to existing events and nobody redeploys the order service. That property is genuinely valuable and hard to achieve any other way.
- Producer availability decoupled from consumer availability.
- Natural fan-out to many independent consumers.
- Load levelling - queues absorb bursts that would overwhelm synchronous callers.
- New consumers added without producer changes.
02What you give up, stated plainly
You lose the call stack. When a synchronous request fails you get a stack trace pointing at the failure. When an event-driven flow fails, you get a message in a dead letter queue, possibly hours later, with no context about what triggered it or what was supposed to happen next. Distributed tracing helps but must be built deliberately, and most teams add it after the first painful incident.
You also lose straightforward ordering and immediate consistency. The user updates their profile, the response returns success, and the search index updates two seconds later. If they search immediately, they see stale data. That is not a bug to fix; it is the architecture working as designed, and your product needs to accommodate it.
03The dual-write problem nobody mentions early
Here is the failure that surprises teams: the service updates its database, then publishes an event. If the publish fails - broker unavailable, process crashes between the two - the database changed and no event was emitted. State is now permanently inconsistent, and nothing detects it.
Reversing the order does not help; you would publish events for transactions that later roll back. The correct solution is the transactional outbox: write the event into an outbox table inside the same database transaction as the state change, then a separate process reads that table and publishes. The database transaction guarantees both or neither, and the publisher retries independently.
04Choosing the right transport
The word 'event' covers several distinct patterns with different infrastructure implications, and conflating them causes real pain. A task queue is not the same as an event log.
| Pattern | Good for | Poor fit for |
|---|---|---|
| Task queue | Background jobs, one consumer per message, retries | Multiple independent consumers of the same event |
| Pub/sub | Fan-out notifications, many consumers, fire and forget | Replay, audit, late-joining consumers |
| Event log | Replay, ordering within a partition, late consumers | Simple background work; operationally heavy |
| Outbox + relay | Guaranteeing state change and event stay consistent | Low-latency delivery requirements |
05Idempotency is mandatory, not optional
Every realistic messaging system delivers at least once. Network partitions, consumer restarts and retry logic all produce duplicates, and a consumer that charges a card or sends an email must handle receiving the same message twice without doing the work twice.
Give every event a stable identifier and record processed identifiers in the same transaction as the effect. If the identifier is already present, acknowledge and skip. This is straightforward when designed in and extremely awkward to retrofit, which is why it belongs in the first consumer you write rather than the incident review after the first duplicate charge.
06A reasonable default
Use synchronous calls when the caller needs the result to proceed and the operation is fast. Use events when multiple consumers care, when the work can happen later, or when producer and consumer availability should be independent. Most healthy systems use both, and the mistake is adopting one exclusively as a matter of principle.
If you are early and uncertain, start synchronous. It is far easier to extract an event-driven path from a working synchronous system than to debug a distributed one you do not yet understand.
Topics
Marcus Hale
Lead Architect · SyncTrix
Writes about the engineering decisions behind production systems - architecture, delivery and the trade-offs that only show up at scale.
Building something like this?
SyncTrix engineers AI, SaaS, platform and cloud systems for enterprises and high-growth teams. Tell us what you're shipping and we'll scope it with you.