Back to Blog
February 7, 20241,000 views

Event-Driven Architecture in System Design: Brokers, Patterns & Delivery Guarantees (Visualized)

Event-driven architecture lets services communicate by emitting and reacting to events instead of calling each other directly. This guide covers events vs commands, brokers like Kafka and RabbitMQ, event sourcing, choreography vs orchestration, delivery guarantees, and the pitfalls of eventual consistency β€” with live animations of each.

Event-driven architecture (EDA) is a design style in which services communicate by producing and reacting to events β€” immutable records that something happened β€” instead of calling each other directly. A producer emits an event, a broker delivers it, and any number of consumers react independently.

In a traditional request/response system, the order service must know about, and synchronously call, the payment service, the email service, and the inventory service. In an event-driven system the order service simply announces OrderPlaced and moves on. Each downstream service subscribes to that event and does its job on its own schedule. This loose coupling is what makes EDA the backbone of scalable, asynchronous systems.

Events vs Commands

The distinction matters. A command is an instruction directed at one recipient β€” ChargeCard β€” and it expects something to be done. An event is a statement of fact about the past β€” CardCharged β€” broadcast to whoever cares. Commands are coupled (the sender knows the receiver and often waits for a result); events are decoupled (the producer does not know or care who consumes them). Naming events in the past tense is a useful discipline because it reminds you they are facts, not requests.

Producers, Consumers, and Brokers

Three roles define every event-driven system. A producer (or publisher) emits events. A broker (the message bus) durably stores and routes them. A consumer (or subscriber) reads and reacts. The broker is the load-bearing wall: it buffers bursts, decouples producers from consumers in time, and fans a single event out to many subscribers. Because the producer never blocks on consumers, a slow or offline downstream service cannot stall the upstream one.

Producer emits events, broker fans out to consumers
A single producer publishes events into the broker, which fans each one out to three independent consumers. Each consumer reacts on its own β€” payment, email, and inventory β€” without the producer knowing they exist.

Choosing a Broker: Kafka, RabbitMQ, SNS/SQS

The broker you pick shapes everything else. Apache Kafka is a distributed, partitioned, append-only log: consumers track their own offset, events are retained for days or replayed from the beginning, and ordering is guaranteed within a partition. RabbitMQ is a traditional message queue with rich routing (exchanges, bindings) where messages are typically deleted once acknowledged. AWS SNS + SQS is a managed fan-out (SNS topics) plus durable per-consumer queues (SQS) that requires no servers to operate.

KafkaRabbitMQSNS + SQS
ModelDistributed log (offsets)Queue + exchangesManaged pub/sub + queues
RetentionDays; replayableUntil acknowledgedUp to 14 days (SQS)
OrderingPer partitionPer queue (best-effort)FIFO queues only
Best forHigh-throughput streams, replayComplex routing, RPC-style workServerless, low-ops fan-out

Three Flavors of Events

Not all events carry the same payload, and the choice has big consequences. With event notification, the event is a thin signal β€” { "orderId": 42 } β€” and consumers call back to fetch details. With event-carried state transfer, the event carries the full data it needs (customer, items, totals), so consumers never call back, trading larger messages for true decoupling. With event sourcing, the events are the source of truth: instead of storing current state, you store the ordered log of every change and rebuild state by replaying it, which gives you a perfect audit trail and time travel for free.

// Event-carried state transfer: consumer needs no callback
{
  "type": "OrderPlaced",
  "orderId": 42,
  "occurredAt": "2024-02-07T10:15:00Z",
  "customer": { "id": 7, "tier": "gold" },
  "items": [{ "sku": "A1", "qty": 2, "price": 1999 }],
  "total": 3998
}

Choreography vs Orchestration

How do you coordinate a multi-step workflow? In choreography, there is no central conductor: each service reacts to events and emits new ones, so the workflow emerges from the chain. OrderPlaced triggers the payment service, whose PaymentCaptured triggers shipping, whose OrderShipped triggers notifications. It is maximally decoupled but the end-to-end flow is implicit and harder to trace. In orchestration, a central orchestrator explicitly issues commands and tracks the saga, which is easier to reason about and monitor but reintroduces a coordinating component.

Choreography: one event triggers a chain
No central conductor. OrderPlaced triggers Payment, which emits PaymentCaptured to trigger Shipping, which emits OrderShipped to trigger Notify. The workflow emerges from services reacting to each other's events.

Delivery Guarantees

Brokers offer three delivery semantics. At-most-once may drop events but never repeats them β€” fine for disposable telemetry. At-least-once guarantees delivery but may repeat events on retry β€” the common default. Exactly-once is the ideal but is expensive and only achievable end-to-end with careful design. The practical answer for at-least-once systems is idempotent consumers: design handlers so processing the same event twice has the same effect as processing it once, usually by deduplicating on a unique event ID.

The Decoupling Payoff

EDA buys you three big things. Loose coupling: you can add a new consumer (say, a fraud detector) without touching the producer. Elastic scaling: the broker absorbs traffic spikes, and you scale slow consumers independently. Resilience: if a consumer is down, events queue up and are processed when it recovers, instead of failing the whole request. This is why event-driven designs underpin microservices, real-time analytics, and IoT pipelines.

Pitfalls: Ordering, Duplicates, and Eventual Consistency

The flip side of decoupling is that consequences are no longer immediate. When a consumer lags, its queue backs up and the rest of the system reads stale data until it catches up β€” this is eventual consistency, and your product must tolerate it. Ordering is only guaranteed within a partition or queue, so events that must be ordered (per user, per account) need a consistent partition key. Duplicates are a fact of life under at-least-once delivery, demanding idempotent handlers. And debugging is harder: a single user action ripples through many services asynchronously, so you need correlation IDs and distributed tracing to follow the thread.

Consumer lag and a backing-up queue
The producer emits faster than the slow consumer can process, so the broker's queue fills up. Backlog and lag climb until the consumer speeds up and drains it β€” the source of eventual consistency.

Request/Response vs Event-Driven

Request/ResponseEvent-Driven
CouplingCaller knows calleeProducer unaware of consumers
TimingSynchronous, blockingAsynchronous, buffered
Failure modeCascades upstreamEvents queue and retry
ConsistencyImmediateEventual
Adding consumersModify the callerSubscribe, no change upstream

Frequently Asked Questions

What is the difference between an event and a command?

A command is a directed instruction to do something (ChargeCard), aimed at one recipient and usually expecting a result. An event is a broadcast statement that something already happened (CardCharged), sent to whoever subscribes. Commands couple sender and receiver; events decouple them.

Is Kafka or RabbitMQ better for event-driven systems?

It depends on your needs. Kafka shines for high-throughput event streams, long retention, and replay, treating events as a durable log. RabbitMQ shines for complex routing and traditional work-queue patterns where messages are consumed and removed. Many systems use both, or a managed option like SNS/SQS for low operational overhead.

How do you handle duplicate events?

Make consumers idempotent. Give every event a unique ID and have the consumer record which IDs it has already processed, so a redelivered event is recognized and skipped. Under at-least-once delivery, duplicates are expected, so idempotency is the safe default rather than chasing true exactly-once semantics.

Event-driven architecture trades immediate certainty for loose coupling and resilience. Embrace eventual consistency and make every consumer idempotent, and the system heals around failure instead of cascading it.

β€” alokknight Engineering