Skip to content

Event-Driven Architecture Patterns โ€‹

๐Ÿ“ Context โ€‹

The customer is considering or already using an event-driven architecture โ€” systems that communicate through events rather than direct calls. Event-driven patterns enable loose coupling, scalability, and real-time reactivity. They also introduce complexity in debugging, ordering, and consistency that must be deliberately managed.

This page is built to be usable on a whiteboard. Each section pairs the concept with a worked example and a talk track โ€” the sentence you actually say when a customer or interviewer asks "okay, but how would that work for us?"

๐Ÿ“‹ Decision Checklist: Should This Be Event-Driven? โ€‹

  • [ ] Producers and consumers have different availability or scaling requirements
  • [ ] Multiple consumers need to react to the same event independently
  • [ ] The system needs to handle bursty or unpredictable load
  • [ ] Temporal decoupling is valuable (producer doesn't need to wait for consumer)
  • [ ] An audit trail of all events is valuable
  • [ ] Eventual consistency is acceptable for this use case

If most of these are no: Synchronous request-response is simpler and easier to debug. Don't add event infrastructure for a system that's fundamentally request-response.

%%{init: {'theme': 'neutral', 'themeVariables': {'fontSize': '14px'}}}%%
flowchart TD
    A[Do multiple consumers need the same event?] -->|Yes| B[Publish-subscribe]
    A -->|No| C[Is this work processed by one worker?]
    C -->|Yes| D[Point-to-point queue]
    C -->|No| E[Is replay or audit trail required?]
    E -->|Yes| F[Event streaming]
    E -->|No| G[Prefer synchronous request-response]

    B --> H[Check ordering and idempotency]
    D --> H
    F --> H
Say it like this

"Before I sketch anything โ€” is this one event that one service consumes, or one event that several teams need to react to independently? That single answer decides whether we reach for a queue or a pub-sub topic."

๐Ÿงฉ Worked Scenario: Order Fan-Out โ€‹

The canonical event-driven case. An e-commerce platform (say, Shopify) records an order. Three downstream systems must react โ€” billing, inventory, and customer notifications โ€” each with different ownership, availability, and scaling needs.

Order fan-out: an order system emits one event consumed independently by billing, inventory, and notification services, each with an idempotency check, behind a reliability layer that retries with backoff, quarantines to a dead letter queue, and alerts for replay.
One order event, three independent consumers, and a reliability layer that retries, quarantines to a DLQ, and alerts for replay.
1 ยท Emit
Order system publishes one order.created event. It does not know or care who consumes it.
2 ยท Fan out
The topic delivers an independent copy to each subscriber. Billing being slow does not block notifications.
3 ยท Consume
Each service processes at its own pace, retries on its own failures, and is independently idempotent.
4 ยท Quarantine
A message that fails past max retries lands in the DLQ โ€” visible and replayable, never silently dropped.

Why this beats direct calls: if the order system called billing โ†’ inventory โ†’ notifications synchronously, one slow or down service would fail the whole order, and adding a fourth consumer later would mean changing the order system. With fan-out, the order system never changes when a new consumer is added.

Say it like this

"The order system emits one fact โ€” 'this order happened' โ€” and walks away. Billing, inventory, and notifications each pick it up on their own schedule. When you add a fourth consumer next quarter, the order system doesn't change a single line."

๐ŸŽฏ Core Patterns โ€‹

Event Types โ€‹

Not all events are the same. Clarify which type you're working with โ€” it changes how much data travels and whether consumers must call back to the source.

TypeDescriptionExampleConsumer Behavior
NotificationSomething happened. Minimal data."Order 123 was placed"Consumer fetches details from source if needed
Event-carried state transferSomething happened, and here's all the data you need."Order 123 placed: {items, total, customer}"Consumer acts without calling back to source
Domain eventA meaningful business occurrence in a bounded context."InventoryReserved"Triggers downstream business logic
CommandA request for action disguised as a message."ProcessPayment"Receiver is expected to act โ€” this is async RPC, not pure event-driven

The trade-off that matters: notifications keep payloads small but create chatty callbacks to the source (and couple consumers to source availability). Event-carried state transfer eliminates the callback but risks stale or bloated payloads and leaks the source's schema to every consumer. Choose per event, not per system.

Messaging Patterns โ€‹

Publish-Subscribe โ€” Producer publishes to a topic; multiple consumers subscribe and receive independently; consumers don't know about each other. Best for fan-out, multiple teams reacting to the same event.

Point-to-Point Queue โ€” Producer sends to a queue; exactly one consumer processes each message; messages consumed once. Best for task distribution, work queues, ordered processing.

Event Streaming โ€” Events written to an append-only log (Kafka, Kinesis, Pulsar); consumers read from an offset and can replay; the log retains events for a configurable window. Best for audit trails, event sourcing, and consumers running at different speeds.

Processing Patterns โ€‹

  • Simple event processing: one event triggers one action. "Order placed" โ†’ "send confirmation email."
  • Event correlation: multiple events combine to trigger an action. "Payment received" + "inventory reserved" โ†’ "ship order." Requires a correlation mechanism (saga, process manager).
  • Complex event processing (CEP): pattern detection across a stream. "More than 5 failed logins in 60 seconds from one IP" โ†’ "block IP." Requires a streaming processor (Flink, KSQL, Spark Streaming).

Delivery Guarantees โ€‹

GuaranteeMeaningImplementation CostWhen Required
At-most-onceMay be lost, never duplicatedLowest โ€” fire and forgetMetrics, analytics where loss is acceptable
At-least-onceWill arrive, possibly more than onceMedium โ€” requires acknowledgment + retryMost business events โ€” consumer must be idempotent
Exactly-onceArrives exactly onceHighest โ€” requires transactions or dedupFinancial transactions, inventory counts

Default to at-least-once with idempotent consumers. Exactly-once is expensive and fragile. Making consumers handle duplicates gracefully is almost always cheaper than achieving true exactly-once delivery โ€” and it degrades better under failure.

Say it like this

"I'd use at-least-once delivery and handle de-duplication on the receiver using the event ID as the key. That's cheaper and more resilient than chasing exactly-once across the network."

๐Ÿ›ก๏ธ Reliability & Error-Handling Layer โ€‹

This is the layer that separates a demo from a production system. When a consumer or destination is down, what happens to the event?

%%{init: {'theme': 'neutral', 'themeVariables': {'fontSize': '14px'}}}%%
flowchart LR
    E[Event arrives] --> Q[Queue]
    Q --> W[Worker processes]
    W --> R{2xx success?}
    R -->|Yes| OK[Ack and done]
    R -->|No| RT[Retry with backoff]
    RT --> X{Max retries hit?}
    X -->|No| W
    X -->|Yes| DLQ[Dead Letter Queue]
    DLQ --> A[Alert + manual replay]

Retry policy โ€” with the numbers stated โ€‹

Vague advice ("use exponential backoff") is what gets you a follow-up question you can't answer. State the actual policy.

AttemptDelayWhy
Retry 1~1sMost transient blips clear immediately
Retry 2~2sDelay doubles each attempt (exponential backoff)
Retry 3~4sBacks off further so a struggling endpoint can recover
After maxCooldown, then DLQStop hammering; quarantine for review/replay
  • Exponential backoff: the retry interval doubles each attempt so you don't hammer a struggling endpoint.
  • Jitter: add randomness to each delay so all consumers don't retry at the exact same instant. Prevents a thundering-herd retry storm.
  • DLQ after max: failed messages move to a dead letter queue for investigation and replay โ€” never silent loss.

Status codes drive the next action โ€‹

The single highest-signal detail in this whole topic. Retrying blindly on every failure is a junior mistake.

ResponseActionReasoning
2xxAcknowledge, doneSuccess
429Back off โ€” never retry immediatelyRate limited; immediate retry makes it worse
5xxRetry with backoffServer-side, likely transient
4xx (not 429)Do not retry โ†’ DLQBad request won't fix itself on retry
Say it like this

"On a 5xx I retry with exponential backoff and jitter. On a 429 I back off โ€” never retry immediately, that just deepens the rate-limit hole. On a 4xx I don't retry at all; the payload is bad, so it goes straight to the dead letter queue for review."

๐Ÿ” Idempotency โ€‹

Definition: an operation is idempotent if running it multiple times produces the same result as running it once. Because at-least-once delivery means the same event can arrive more than once, every consumer doing a state change must handle duplicates safely.

The mechanism: an idempotency key โ€” a unique ID on the event, usually the source's event or record ID. Before processing, check whether you've seen this key. If yes, skip. If no, process and record the key.

ScenarioRisk without idempotencyFix
Payment event fires twiceCustomer charged twiceCheck event ID before processing; store processed IDs
Record created twiceDuplicate data in destinationUpsert on external ID, not blind insert
Retry after a timeoutFirst delivery succeeded but the response was lostAt-least-once + idempotency key = safe
Say it like this

"I assume every event can be delivered twice, so consumers upsert on the source record ID instead of inserting. The duplicate becomes a no-op instead of a double charge."

๐Ÿ‘๏ธ Observability โ€” Who Sees What โ€‹

A reliable system that nobody can see into still generates support tickets. Decide deliberately what each audience sees.

AudienceWhat they seeWhy it matters
EngineeringFull execution logs, payload inspection, step-by-step traceDiagnose failures fast without touching production
Customer SuccessPer-customer status, error summaries, a replay buttonCS resolves common issues without pulling in engineers
End customerHealth status, last-run time, self-serve retryTrust โ€” they don't file tickets for things they can fix
AlertingStream to Datadog / PagerDuty / Slack on threshold breachCatch failures before the customer reports them
Say it like this

"The goal is that a failure is visible and recoverable, not silent. Engineering gets the stack trace, CS gets a replay button, and the customer gets a health indicator โ€” so most issues never become a ticket."

๐ŸŽฏ Technology Selection โ€‹

TechnologyPatternOrderingRetentionBest For
AWS SQSQueuePer-queue (FIFO) or best-effort14 days maxSimple work queues, decoupled processing
AWS SNSPub-subNo ordering guaranteeNoneFan-out notifications
Apache KafkaStreamingPer-partitionConfigurable (days to indefinite)High-throughput streaming, event sourcing
AWS KinesisStreamingPer-shard1โ€“365 daysAWS-native streaming alternative to Kafka
RabbitMQQueue + Pub-subPer-queueUntil consumedFlexible routing, complex topologies
Azure Service BusQueue + Pub-subPer-queue (sessions)ConfigurableEnterprise messaging with transactions
Google Pub/SubPub-subPer-subscription (ordering key)31 daysGCP-native event distribution

Selection Criteria โ€‹

  • Throughput: events per second? Kafka and Kinesis handle millions; SQS handles tens of thousands.
  • Ordering: must events process in order? Kafka guarantees per-partition; SQS FIFO guarantees per-group.
  • Retention: need to replay? Kafka retains indefinitely; SQS discards after consumption.
  • Operational burden: managed (SQS, SNS, Pub/Sub) vs. self-managed (Kafka โ€” powerful but operationally expensive).
  • Ecosystem fit: already run Kafka? Don't add SQS. Already on AWS for simple queuing? Don't add Kafka.

โš ๏ธ Gotchas โ€‹

  • Treating everything as an event โ€” synchronous calls are fine when you need an immediate response.
  • No dead letter queue โ€” failed messages disappear silently and data is lost.
  • Ordering assumptions โ€” most systems guarantee only per-partition/group order, never global.
  • Consumer lag going unmonitored โ€” a consumer falling behind is a ticking time bomb.
  • Not making consumers idempotent โ€” at-least-once delivery means duplicates happen.
  • Event schemas without versioning โ€” schema changes silently break consumers.
  • "We'll just use Kafka" for simple queuing โ€” SQS handles simple cases with zero operational overhead.
  • No replay strategy โ€” when something breaks, you want to reprocess from a known point.

๐Ÿ“š Further reading โ€‹

Foundational writing on events, streaming, and consistency:

Built as a public field guide for practical Solutions Engineering and Architecture work.