Hermes Wiki

Event-Driven Architecture

Concept

Event-Driven Architecture (EDA) is an architectural style where components communicate by producing and reacting to events — immutable facts about something that already happened ("OrderPlaced", "PaymentFailed") — instead of one component directly calling another and waiting for a response. This is the opposite orientation of request/response: a request/response caller knows exactly who it's calling and blocks until it gets an answer; an event producer publishes a fact and moves on, with zero knowledge of who (if anyone) is listening. That inversion is what gives EDA its core property — producers and consumers are decoupled from each other's existence, not just their implementation details.

EDA is a style, not a single technology, and it's commonly implemented on top of two different kinds of infrastructure with different delivery guarantees: a message queue (e.g. Amazon SQS, RabbitMQ) delivers each message to exactly one consumer and removes it once processed — good for distributing work items across a pool of workers. A pub/sub event bus or event stream (e.g. Amazon EventBridge, Google Cloud Pub/Sub, Kafka) instead fans a single published event out to every interested subscriber, and often retains a durable log of events rather than deleting them once read. A common real architecture combines both: a topic fans an event out via pub/sub, and each subscriber has its own queue behind it so that a slow consumer doesn't block a fast one and each gets its own retry/dead-letter handling. AWS's canonical version of this is SNS fanning out to per-consumer SQS queues.

A second axis worth naming explicitly: event notification (a small event carrying just an ID — "OrderPlaced, id=123" — forcing the consumer to call back for details) versus event-carried state transfer (the event itself carries the full data the consumer needs, avoiding the callback but risking a bloated, tightly-coupled-to-the-producer's-internal-shape payload). Most production systems land somewhere in between, deliberately choosing per event type based on how much data consumers actually need and how often the producer's internal shape changes.

Tradeoffs

Approach Benefit Cost
Request/response (direct calls) Simple to trace, caller knows immediately whether the call succeeded Producer and consumer are coupled in time (both must be up) and in knowledge (caller must know who to call)
Message queue (point-to-point) Exactly one consumer processes each message; natural work-distribution across a worker pool Doesn't fan out — adding a second interested consumer means adding a second queue and duplicating the publish, not just subscribing
Pub/sub event bus Many independent consumers can subscribe without the producer knowing or caring; new consumers plug in with zero producer changes At-least-once delivery is the norm, not exactly-once — consumers see duplicates and must be idempotent; a bug in event routing/filtering is harder to spot than a broken direct call
Event-carried state transfer Consumers don't need a callback to get details, lower latency, works even if producer is briefly down Payload grows with producer's internal model; a schema change in the producer's data model now breaks every consumer's parsing, not just one API contract

The deeper tradeoff underneath all four rows is the same one: EDA trades the caller's immediate certainty ("did this succeed, right now") for looser coupling and independent scaling — and that trade is only worth making where the certainty wasn't actually needed for the process to be correct.

When to use / when not to

  • Use EDA when multiple independent parts of the system need to react to the same fact and the producer shouldn't need to know or care who's listening — new consumers (a new analytics pipeline, a new notification channel) should be addable without touching the producer.
  • Use a message queue specifically when the goal is distributing discrete units of work across a pool of workers, where each item should be handled exactly once by exactly one worker.
  • Use a pub/sub bus specifically when the goal is broadcasting a fact to an unknown or growing number of interested parties.
  • Don't reach for EDA where a caller genuinely needs a synchronous answer to proceed — checking out a shopping cart needs to know payment succeeded before showing a confirmation page; that's a request/response concern even in an otherwise event-heavy system, not a candidate for "fire an event and hope."
  • Don't choose event-carried state transfer for high-churn internal data models without discipline around schema versioning — see the schema-drift pitfall below.

Common pitfall

Standing up a pub/sub bus and treating "at-least-once delivery" as a detail rather than a design requirement. Every mainstream event bus (SNS, EventBridge, Pub/Sub, Kafka) can and will redeliver a message — a consumer crash mid-processing, a network blip during acknowledgment, or the broker's own retry logic can all cause the same event to arrive twice. A consumer written as if delivery were exactly-once (e.g. "increment the order count by 1" on every event received) silently corrupts data the first time a duplicate slips through, often invisibly for weeks. The fix is designing every consumer to be idempotent from day one — keying processing off the event's unique ID and making the handler's effect the same whether it runs once or five times — rather than treating idempotency as a hardening pass to add later once duplicates are observed in production.

Engineering Lens

The strongest EDA design decisions are made by naming, per event type, exactly which of the four tradeoff rows above applies and why — not by picking "we use events" as a single blanket architectural stance. A design review answer worth trusting says something like: "OrderPlaced is pub/sub with event-carried state because five independent teams need the full order without a callback, but PaymentAuthorization is synchronous because checkout can't render a confirmation without knowing the result." The failure mode to watch for is EDA adopted for its architectural appeal — decoupling sounds unambiguously good — without anyone owning the operational cost it introduces: distributed tracing becomes mandatory once a business process spans several independently-triggered event handlers, and "what happened to this specific order" stops being answerable by reading one service's logs.

Sources

Hermes Wiki