Event-Driven Architecture
Software Architecture Fundamentals
Chapter 6 · Event-Driven Architecture
Chapter 5 ended with two verified boundary functions — send_order_confirmation_email and update_user_loyalty_points — that both needed direct knowledge of both the Order and User domains to work. This chapter rebuilds them without that direct knowledge, using the exact mechanism Design Patterns Chapter 8 already gave you: publish/subscribe.
The Event Bus: Observer, Generalized for Typed Events
UserService and EmailService subscribe to 'OrderPlaced' independently — OrderService never references either one by name:
OrderService's source via inspect.getsource(): the string 'UserService' appears False times, and 'EmailService' appears False times. Calling order_service.place_order('ORD-1', 100, 'USER-1') correctly awarded 10 loyalty points (100 // 10) to USER-1 and correctly queued the confirmation email — both through subscriptions OrderService has no knowledge of.
AnalyticsService.log_order as a new subscriber (via one bus.subscribe('OrderPlaced', analytics_service.log_order) call) and placing a second order correctly logged it — while OrderService's own code was never touched. This is the exact same "add an observer, zero changes to the subject" finding Design Patterns Chapter 8 verified for Stock, now applied one level up: to services instead of objects.
Comparing Chapter 5's Direct Version Against This Chapter's Event-Driven Version
| Chapter 5 — direct calls | Chapter 6 — pub/sub events | |
|---|---|---|
| Does OrderService know UserService exists? | Yes — send_order_confirmation_email calls get_user_profile directly | No — verified: zero source references |
| Adding a new reaction to "order placed" | Edit an existing function to add a new call | Add a new subscriber — verified: zero OrderService changes |
| What happens if a reaction is slow or fails? | The whole call chain waits or fails together (Chapter 4's cascading-call finding) | Isolated per subscriber — covered further in this course's own resilience-pattern chapter |
A Basic Event-Sourcing Example
Instead of only reacting to events as they happen, an EventLog can record every event permanently — making the events themselves the source of truth, not just a running total.
OrderPlaced events (100, 50, 75) through an EventBus that logs every event before dispatching it, replay_total_revenue(log) returns 225, matching a manual sum (100+50+75) exactly. Filtering the same log for USER-1's own events reconstructs their order count (2) and total spend (150) — every one of these numbers comes entirely from replaying the stored events, not from a separately-maintained total that could drift out of sync.
The Eventual Consistency Tradeoff, Measured
Chapter 4's cascading-call demo showed a synchronous chain making a caller wait for every downstream step. An asynchronous event bus avoids that wait — by genuinely not having the update happen yet.
order_service.place_order('ORD-1', 100, 'USER-1') against an AsyncEventBus returns successfully, and order_service.orders correctly shows the new order. But checking user_service.users immediately afterward shows {} — genuinely empty. Only after bus.process_queue() runs does user_service.users correctly show {'USER-1': {'points': 10}}. Between those two points, the system is honestly, verifiably inconsistent — the order is real, but the loyalty points aren't there yet.
send_order_confirmation_email/update_user_loyalty_points returned — but only by giving OrderService direct knowledge of both other domains. This chapter's event-driven version genuinely doesn't know or care who's listening, or when they'll get around to processing the event — and that's precisely what creates the gap just measured. Neither tradeoff is free; which one to accept depends on whether "the user sees their points immediately" or "OrderService never has to know UserService exists" matters more for a given system.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| The event bus verified as Observer, reused a second time (after MVVM in Chapter 3) | Confirms this course's own recurring theme — design-level patterns are the concrete building blocks architecture-level decisions are actually made of |
| A real, measured eventual-consistency window | Distributed Systems & Scalability's own CAP Theorem & Consistency Models chapter — this chapter's own small demo is a concrete instance of that larger tradeoff |
| Event-sourcing reconstructing exact totals purely from a log | Technical Support's own `log1`/`backup1` — an event log serving the same "the record is the source of truth" role a well-kept audit log serves there |
Hands-On Exercises
Add a fourth subscriber, InventoryService.reserve_stock, to this chapter's own synchronous EventBus example, subscribed to 'OrderPlaced'. Verify it fires correctly alongside the existing three subscribers, and confirm OrderService's own source still contains zero reference to it.
Using this chapter's own EventLog, write a replay_orders_over(event_log, threshold) function that reconstructs — purely by replaying the log, no running counter — the count of orders with items_total greater than threshold. Verify it against this chapter's own three-event log (100, 50, 75) with a threshold of 60.
Using this chapter's own verified eventual-consistency finding, explain what a user-facing "Order placed! Your loyalty points will update shortly" message is actually doing — and why Chapter 5's original direct-call version would never have needed a message like that at all.
📄 View solutionChapter 6 Quick Reference
- Event bus: publishers publish typed events with no knowledge of subscribers — verified:
OrderService's own source contained zero reference to either subscriber, and a third subscriber was added with zeroOrderServicechanges - Event sourcing: the event log itself is the source of truth — verified: exact revenue (
225) and per-user totals reconstructed purely by replaying stored events - Eventual consistency, measured: a real, verified window existed where
order_service.ordersreflected the new order butuser_service.userswas still empty, closed only once the queued event was processed - Next chapter: Hexagonal / Clean Architecture — pushing the same dependency-direction discipline even further