Every mechanism this domain covered is genuinely useful, and every one of them is also a way to build a system that's harder to understand than the synchronous one it replaced — this closing lesson is about the specific ways that actually happens, and why it happens to well-intentioned teams, not careless ones.
3 min read
Each individual step above looks reasonable in isolation — a service reacts to one event by doing its job and publishing the next. The problem emerges from the chain as a whole: understanding "what happens when an order is placed" now requires tracing through six services' independent event handlers, none of which has the full picture, and a failure three steps in (shipping scheduling fails) leaves the system in a state no single piece of code ever explicitly modeled — inventory reserved and payment charged, but nothing shipped, with no service responsible for recognizing or resolving that combination.
A system is a distributed monolith when its services are deployed independently (the thing microservices are supposed to buy you) but still have to change together constantly, because each one's event handlers assume exact knowledge of every other service's event shapes and ordering — exactly the tight coupling a monolith has, minus the one advantage a monolith actually offers: a single codebase and a single stack trace when something goes wrong. Lesson 10's schema-evolution discipline and lesson 1's deliberate choice between notification and state-transfer patterns exist specifically to prevent this outcome — skip them, and implicit coupling creeps in anyway, just distributed across repositories instead of contained in one.
A synchronous call chain leaves one continuous stack trace, in one process, showing exactly where something failed. An event chain spans multiple processes, multiple queues, and real time gaps between steps — by the time step 4 fails, steps 1 through 3 already committed and moved on, and nothing connects them into one traceable unit by default. Distributed tracing (covered in depth in the Observability & Monitoring domain) exists specifically to stitch an event chain back into one followable trace, by propagating a shared trace ID through every event in the chain — without it, debugging an event-driven flow means manually correlating timestamps and IDs across several services' separate logs.
CQRS's read-model lag (lesson 6) and the outbox relay's small delay (lesson 7) are each individually reasonable trade-offs — but stacked across a real chain of several services, the cumulative lag between "a user took an action" and "every part of the system reflects it" can become large enough for a user to notice directly: placing an order and seeing an order history page that doesn't show it yet, because the read model three hops downstream hasn't caught up. Designing for this explicitly (a UI that shows "processing" rather than silently looking wrong) is a real requirement event-driven systems introduce that a purely synchronous system doesn't.
Message queues, pub/sub, Kafka's log model, event sourcing, CQRS, the outbox pattern — every one solves a real problem this domain named specifically, and every one adds genuine complexity that isn't free. A system reaching for all of them because they sound like "the right way to build modern software," rather than because a specific, named problem (decoupling a slow dependency, needing a replayable audit log, read/write patterns that genuinely diverge) actually exists, ends up with a distributed monolith's coupling and an event-driven system's debugging difficulty, with none of the benefit either architecture was supposed to provide.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What's the core problem with long event chains, even when each individual step looks reasonable?
2. What defines a "distributed monolith"?
3. What is the lesson's actual closing argument about the patterns covered in this domain?
Message Queues & Event-Driven Architecture