SyncTrix logoSyncTrix
All articles
Platform11 min read

Event-driven architecture: when it helps and when it just adds latency

Events decouple services and destroy your ability to reason about ordering. Here's an honest account of what you gain, what you give up, and when the trade is worth it.

By Marcus Hale
Event-driven architecture: when it helps and when it just adds latency

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.

PatternGood forPoor fit for
Task queueBackground jobs, one consumer per message, retriesMultiple independent consumers of the same event
Pub/subFan-out notifications, many consumers, fire and forgetReplay, audit, late-joining consumers
Event logReplay, ordering within a partition, late consumersSimple background work; operationally heavy
Outbox + relayGuaranteeing state change and event stay consistentLow-latency delivery requirements
Messaging patterns and their fit

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

event driven architecturemessage queuekafkaeventual consistencymicroservices communicationoutbox pattern

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.