Event-Driven Architecture · lesson 1 of 10
Beginner
3Intermediate
6- 4 Event Sourcing: Snapshots So Replay Doesn't Get Slower Forever
- 5 CQRS: Separate Write and Read Models, and the Lag Between Them
- 6 The Dual-Write Problem: A DB Commit and a Publish Can Fail Independently
- 7 Transactional Outbox: Committing the Event With the Data
- 8 Idempotent Consumers: Surviving a Duplicate Delivery
- 9 Dead-Letter Queues: What Happens When Retries Keep Failing
Also part of Event-Driven Architecture
6- + Saga Pattern: Orchestrating a Multi-Step Transaction from Microservices
- + Saga Pattern: Real Compensation When a Step Fails from Microservices
- + At-Most-Once, At-Least-Once, and Exactly-Once from Kafka
- + Serialization and the Schema Registry from Kafka
- + Pub/Sub: Channels, Patterns, and the Message That Never Arrives from Redis
- + Incremental Loads and Change Data Capture from Data Engineering
Also part of: System Design Microservices
Events vs Requests: Why the Order Service Doesn't Wait for Email
A synchronous order flow fails outright when email is down; an event-driven one records the order and lets each handler fail independently.
By SkillByExample · Published
A request-driven order flow calls each step directly and waits for it to finish; an event-driven one just announces what happened and lets independent handlers react.
The same failure, two very different outcomes
class EmailServiceDown(Exception):
pass
def send_confirmation_email(order_id):
raise EmailServiceDown("email service is down")
def reserve_inventory(order_id):
print(f" inventory reserved for order {order_id}")
def place_order_request_driven(order_id):
print(f"Placing order {order_id} (request-driven)...")
reserve_inventory(order_id)
send_confirmation_email(order_id)
print(f"Order {order_id} placed successfully")
try:
place_order_request_driven(101)
except EmailServiceDown as e:
print(f"Order 101 FAILED: {e}")
print()
event_log = []
def place_order_event_driven(order_id):
print(f"Placing order {order_id} (event-driven)...")
event_log.append({"type": "OrderPlaced", "order_id": order_id})
print(f"Order {order_id} placed successfully")
def inventory_handler(event):
if event["type"] == "OrderPlaced":
print(f" [inventory handler] reserved stock for order {event['order_id']}")
def email_handler(event):
if event["type"] == "OrderPlaced":
try:
send_confirmation_email(event["order_id"])
except EmailServiceDown as e:
print(f" [email handler] failed, will retry later: {e}")
place_order_event_driven(102)
for event in event_log:
inventory_handler(event)
email_handler(event)
$ python events_vs_requests.py
Placing order 101 (request-driven)...
inventory reserved for order 101
Order 101 FAILED: email service is down
Placing order 102 (event-driven)...
Order 102 placed successfully
[inventory handler] reserved stock for order 102
[email handler] failed, will retry later: email service is down
Order 101 never gets placed at all — send_confirmation_email raising partway through
place_order_request_driven unwinds the whole function, so a message queue outage takes down
order placement with it. Order 102 succeeds the moment its event is logged; the email failure
happens later, inside a handler that has nothing to do with whether the order itself is
considered placed.
Practice
1. Add a third handler, analytics_handler, and confirm it runs independently of the other two.
event_log = [{"type": "OrderPlaced", "order_id": 201}]
def inventory_handler(event):
if event["type"] == "OrderPlaced":
print(f" [inventory handler] reserved stock for order {event['order_id']}")
def email_handler(event):
if event["type"] == "OrderPlaced":
print(f" [email handler] confirmation sent for order {event['order_id']}")
def analytics_handler(event):
print(f" [analytics handler] recorded {event['type']} for order {event['order_id']}")
for event in event_log:
inventory_handler(event)
email_handler(event)
analytics_handler(event)
$ python add_analytics_handler.py
[inventory handler] reserved stock for order 201
[email handler] confirmation sent for order 201
[analytics handler] recorded OrderPlaced for order 201
analytics_handler doesn’t check event["type"] at all — it just logs whatever comes through.
Adding it required no change to the other two handlers or to how events get produced.
2. Make reserve_inventory fail instead of send_confirmation_email, and compare both flows again.
class InventoryUnavailable(Exception):
pass
def reserve_inventory_fails(order_id):
raise InventoryUnavailable("warehouse system unreachable")
def send_confirmation_email(order_id):
print(f" confirmation email sent for order {order_id}")
def place_order_request_driven(order_id):
print(f"Placing order {order_id} (request-driven)...")
reserve_inventory_fails(order_id)
send_confirmation_email(order_id)
print(f"Order {order_id} placed successfully")
try:
place_order_request_driven(301)
except InventoryUnavailable as e:
print(f"Order 301 FAILED: {e}")
print()
event_log = []
def place_order_event_driven(order_id):
print(f"Placing order {order_id} (event-driven)...")
event_log.append({"type": "OrderPlaced", "order_id": order_id})
print(f"Order {order_id} placed successfully")
def inventory_handler(event):
try:
reserve_inventory_fails(event["order_id"])
except InventoryUnavailable as e:
print(f" [inventory handler] failed, will retry later: {e}")
def email_handler(event):
send_confirmation_email(event["order_id"])
place_order_event_driven(302)
for event in event_log:
inventory_handler(event)
email_handler(event)
$ python fail_inventory_instead.py
Placing order 301 (request-driven)...
Order 301 FAILED: warehouse system unreachable
Placing order 302 (event-driven)...
Order 302 placed successfully
[inventory handler] failed, will retry later: warehouse system unreachable
confirmation email sent for order 302
Whichever step fails, the request-driven version loses the entire order; the event-driven version only loses whatever that one handler was responsible for. The failure moves, but the pattern’s behavior doesn’t.
3. Add a second event type, OrderCancelled, and have handlers react differently to each.
event_log = [
{"type": "OrderPlaced", "order_id": 401},
{"type": "OrderCancelled", "order_id": 401},
]
def inventory_handler(event):
if event["type"] == "OrderPlaced":
print(f" [inventory handler] reserved stock for order {event['order_id']}")
elif event["type"] == "OrderCancelled":
print(f" [inventory handler] released stock for order {event['order_id']}")
def email_handler(event):
if event["type"] == "OrderPlaced":
print(f" [email handler] confirmation sent for order {event['order_id']}")
else:
print(f" [email handler] ignoring {event['type']}, nothing to send")
for event in event_log:
print(f"--- handling {event['type']} ---")
inventory_handler(event)
email_handler(event)
$ python two_event_types.py
--- handling OrderPlaced ---
[inventory handler] reserved stock for order 401
[email handler] confirmation sent for order 401
--- handling OrderCancelled ---
[inventory handler] released stock for order 401
[email handler] ignoring OrderCancelled, nothing to send
Each handler decides for itself which event types matter to it — email_handler explicitly
ignores OrderCancelled instead of the event log needing to know which handlers care about
which types.
4. Count how many events are left unresolved after a batch, to see what a real retry mechanism would need to track.
class EmailServiceDown(Exception):
pass
def send_confirmation_email(order_id):
if order_id % 2 == 0:
raise EmailServiceDown("email service is down")
print(f" [email handler] confirmation sent for order {order_id}")
event_log = [
{"type": "OrderPlaced", "order_id": 501},
{"type": "OrderPlaced", "order_id": 502},
{"type": "OrderPlaced", "order_id": 503},
]
unresolved = []
for event in event_log:
try:
send_confirmation_email(event["order_id"])
except EmailServiceDown:
print(f" [email handler] failed for order {event['order_id']}, queued for retry")
unresolved.append(event)
print(f"Unresolved events needing retry: {len(unresolved)}")
print([e["order_id"] for e in unresolved])
$ python count_unresolved.py
[email handler] confirmation sent for order 501
[email handler] failed for order 502, queued for retry
[email handler] confirmation sent for order 503
Unresolved events needing retry: 1
[502]
unresolved is exactly the bookkeeping a real retry mechanism needs — which events a handler
never successfully processed — and it exists here as a plain list because there’s only one
consumer; a real system with many independent consumers needs this tracked per-consumer, not
once globally.