Event-Driven Architecture: Decoupling Services the Right Way
How to build loosely-coupled systems with event-driven patterns. Covers message brokers, event sourcing, and common pitfalls.
March 4, 2026 · 8 min · 1622 words · Rob Washington
Table of Contents
Synchronous HTTP calls create tight coupling. Service A waits for Service B, which waits for Service C. One slow service blocks everything. One failure cascades everywhere.
# Instead of: UPDATE accounts SET balance = 150 WHERE id = 1# Store events:events=[{"type":"AccountOpened","account_id":1,"initial_balance":100},{"type":"DepositMade","account_id":1,"amount":75},{"type":"WithdrawalMade","account_id":1,"amount":25},]# Derive current state by replaying eventsdefget_balance(account_id):balance=0foreventinget_events(account_id):ifevent["type"]=="AccountOpened":balance=event["initial_balance"]elifevent["type"]=="DepositMade":balance+=event["amount"]elifevent["type"]=="WithdrawalMade":balance-=event["amount"]returnbalance
Events may be delivered more than once. Make handlers idempotent:
1
2
3
4
5
6
7
8
9
10
defhandle_order_created(event):# Check if already processedifdb.exists("processed_events",event.id):return# Skip duplicate# Processreserve_inventory(event.order_id,event.items)# Mark as processeddb.insert("processed_events",{"event_id":event.id,"processed_at":now()})
defprocess_message(message):try:handle_event(message)channel.basic_ack(message.delivery_tag)exceptExceptionase:ifmessage.redelivery_count>3:# Send to dead letter queuechannel.basic_publish(exchange='dead-letters',routing_key=message.routing_key,body=message.body)channel.basic_ack(message.delivery_tag)else:# Retry laterchannel.basic_nack(message.delivery_tag,requeue=True)
Event-driven systems are eventually consistent. Embrace it:
1
2
3
4
5
6
7
8
9
10
11
# Don't do this: expect immediate consistencydefplace_order(order):publish(OrderCreated(order))# BAD: inventory might not be updated yetreturnget_inventory_status(order.items)# Do this: design for eventual consistencydefplace_order(order):publish(OrderCreated(order))return{"status":"processing","order_id":order.id}# Client polls or subscribes for updates
Tell users “Your order is being processed” instead of showing stale data.
# Consumer lag: how far behind are we?consumer_lag=latest_offset-consumer_offset# Processing rateevents_processed_per_second=Counter('events_processed')# Error rateevents_failed=Counter('events_failed',['event_type','error'])# End-to-end latencyevent_latency=Histogram('event_latency_seconds')
Alert on:
Consumer lag growing
Dead letter queue filling up
Processing latency spikes
Event-driven architecture trades immediate consistency for loose coupling and resilience. Use it where those tradeoffs make sense.
📬 Get the Newsletter
Weekly insights on DevOps, automation, and CLI mastery. No spam, unsubscribe anytime.