Module 3: Event-Driven Architecture: Case Studies
These case studies focus on event semantics, delivery guarantees, ordering, sagas, and the operational cost of asynchronous systems.
Case Study 1: Transactional Outbox For Order Events
Scenario: Checkout writes an order row and publishes OrderCreated to a broker. The database commit succeeds, the broker publish fails, and fulfillment never hears about the order.
Source anchor: Chris Richardson's Transactional Outbox, which addresses updating a database and sending messages without a distributed transaction.
Module concepts: outbox, dual-write failure, relay, idempotent consumer, at-least-once delivery.
Wrong Approach
Write to the database and broker in the same function and hope both succeed.
Better Approach
Use the database as the atomic boundary:
Transaction:
insert order
insert outbox event
commit
Relay:
reads unsent outbox rows
publishes to broker
marks sent or relies on dedupe
Tradeoff Table
| Choice | Gain | Cost |
|---|---|---|
| direct publish | simple | lost events on partial failure |
| outbox | atomic DB/event record | relay and dedupe complexity |
| 2PC | atomic across resources | blocking/ops complexity |
| change data capture | low app coupling | CDC infrastructure |
Required Artifact
Design an outbox table, relay retry policy, dedupe key, and consumer idempotency rule.
Case Study 2: Kafka Exactly-Once Is Not End-To-End Exactly Once
Scenario: A payments event processor uses Kafka idempotent producers and transactions. The team assumes duplicate charges are impossible.
Source anchor: Apache Kafka producer configuration docs, including transactional.id and idempotence behavior. See Apache Kafka producer configs.
Module concepts: idempotent producer, transactional producer, exactly-once semantics, external side effects.
Wrong Approach
"Kafka exactly-once means our whole business workflow is exactly-once."
Kafka can provide guarantees inside Kafka's read-process-write boundary. External APIs and databases still need idempotency.
Better Approach
Draw the end-to-end boundary:
Kafka input topic
-> processor
-> Kafka output topic: transactional
-> payment provider API: needs idempotency key
-> database write: needs unique operation key
Tradeoff Table
| Choice | Gain | Cost |
|---|---|---|
| at-least-once + idempotent consumer | robust and explicit | dedupe storage |
| Kafka transactions | stronger stream processing | config/operational complexity |
| external idempotency keys | protects side effects | API/storage contract |
| assume exactly once | easy story | duplicate business effects |
Required Artifact
Write an end-to-end processing contract that identifies every duplicate boundary.
Case Study 3: Event Notification vs Event-Carried State
Scenario: CustomerUpdated contains only customer_id. Every consumer calls Customer Service to fetch details, creating a thundering herd during batch updates.
Source anchor: Martin Fowler's Event-Driven article distinguishes event notification from event-carried state transfer.
Module concepts: event notification, event-carried state, coupling, consumer autonomy.
Wrong Approach
Make every event tiny without considering consumer needs.
Better Approach
Choose payload shape by consumer contract:
Notification:
customer_id only, consumers fetch current state
Carried state:
customer_id, email, plan, status, version
Tradeoff Table
| Choice | Gain | Cost |
|---|---|---|
| notification | small events | producer service read dependency |
| carried state | consumer autonomy | duplicated data and schema evolution |
| full snapshot | easy projection | large events |
| delta event | compact | harder replay |
Required Artifact
Write an event schema ADR comparing notification, delta, and carried-state payloads.
Case Study 4: Saga For Trip Booking
Scenario: A trip booking workflow reserves flight, hotel, and payment. Hotel reservation fails after flight succeeds. The system cannot use one ACID transaction across providers.
Source anchor: Microsoft Azure's Compensating Transaction pattern describes undo operations, idempotency, and resumable workflows.
Module concepts: saga, compensation, orchestration, choreography, idempotency.
Wrong Approach
Pretend a distributed workflow is a local transaction.
Better Approach
Use an explicit saga:
ReserveFlight -> ReserveHotel -> ChargeCard
If hotel fails:
CancelFlight
ReleasePaymentHold
Every step and compensation gets an idempotency key and persisted state.
Tradeoff Table
| Choice | Gain | Cost |
|---|---|---|
| 2PC | atomicity | poor provider fit |
| saga orchestration | visible workflow | coordinator state |
| choreography | service autonomy | harder global reasoning |
| manual review | handles rare ambiguity | ops burden |
Required Artifact
Draw a saga state machine with compensations, retry rules, timeout behavior, and manual review states.
Case Study 5: Partition Ordering In A Stream
Scenario: Inventory events are partitioned by event ID. Two updates for the same SKU land in different partitions and are processed out of order.
Source anchor: Kafka's documentation explains producers, partitions, and key-based partitioning behavior through configuration and client docs. See Apache Kafka documentation.
Module concepts: partition key, ordering guarantee, consumer group, hot partitions.
Wrong Approach
"Kafka preserves order" without saying within what scope.
Better Approach
Partition by the entity whose order matters:
Need order per SKU:
partition key = sku
Need order per customer:
partition key = customer_id
Need global order:
one partition or external sequencer, with scale cost
Tradeoff Table
| Choice | Gain | Cost |
|---|---|---|
| key by event ID | even distribution | no entity order |
| key by SKU | per-SKU order | hot SKU risk |
| one partition | total order | poor throughput |
| repartition stream | fixes downstream shape | extra pipeline |
Required Artifact
Write a partition-key decision note: order scope, skew risk, partition count, rebalance behavior, and consumer idempotency.
Source Map
| Source | Use it for |
|---|---|
| Transactional Outbox | atomic DB update plus event publication |
| Apache Kafka producer configs | idempotence and transactional producer settings |
| Martin Fowler: Event-Driven | event notification vs carried state |
| Azure Compensating Transaction | saga compensation and idempotency |
| Apache Kafka documentation | partitions, ordering, producers, consumers |
Completion Standard
- At least three artifacts are completed.
- At least one artifact includes outbox schema and relay behavior.
- At least one artifact identifies an ordering scope.
- At least one artifact includes saga compensation.