Event-Driven Architecture
In a request-driven system, services call each other: “create this order,” “charge this card.” In an event-driven system, services announce what happened — “an order was placed” — and anyone interested reacts. This inversion is one of the most powerful (and most over-applied) ideas in system design. Understanding when it helps is the whole game.
Commands vs events
Section titled “Commands vs events”The distinction is about intent and ownership:
COMMAND "ChargeCard" → directed at ONE service, expects it to act. Coupling: caller knows callee.EVENT "OrderPlaced" → a fact, broadcast. Zero, one, or many consumers react. Caller knows no one.A command says do this; an event says this happened. The shift from commands to events is what
decouples the producer from its consumers — the order service doesn’t know (or care) that email,
analytics, and inventory all react to OrderPlaced.
The shape of an event-driven system
Section titled “The shape of an event-driven system”Producers publish to a log or broker (see Message Queues); consumers subscribe.
┌─────────────► Email service Order service ──"OrderPlaced"──► [ event log ] ──► Inventory service └─────────────► Analytics (publishes once, knows nothing about who consumes)Add a new consumer (say, a loyalty-points service) and you change nothing in the producer. That extensibility is the headline benefit.
Two flavours: choreography vs orchestration
Section titled “Two flavours: choreography vs orchestration”When a business process spans services, you choose how it’s coordinated:
| Choreography | Orchestration | |
|---|---|---|
| Control | Each service reacts to events independently | A central coordinator directs each step |
| Coupling | Loose; no one “owns” the flow | Tighter; the orchestrator knows the flow |
| Visibility | Hard — the flow is emergent | Easy — one place describes the whole saga |
| Best for | Simple, stable reactions | Complex multi-step workflows needing rollback |
Choreography is elegant until you need to answer “why didn’t this order ship?” and discover the logic is scattered across six services. Orchestration re-centralizes that at the cost of a coordinator.
Event sourcing (a related idea)
Section titled “Event sourcing (a related idea)”Event sourcing stores the sequence of events as the source of truth instead of just current state. Current state is derived by replaying events.
Traditional: store balance = $80Event-sourced: store [ +100 deposited, -20 withdrawn ] → replay → balance = $80This buys a perfect audit log and the ability to reconstruct any past state — at the cost of more storage, replay complexity, and the need for snapshots so you don’t replay millions of events.
The trap: the dual-write problem
Section titled “The trap: the dual-write problem”Here’s the failure that bites every event-driven system eventually. A service often needs to update its database AND publish an event. Those are two separate systems, and you cannot make them atomic:
1. write order to DB ✓2. publish "OrderPlaced" ✗ (crash / broker down) → DB says order exists, but no one was told. State diverges.You can’t fix this by reordering — publish first and the DB write might fail instead. The robust solution is the transactional outbox pattern, covered in The Dual-Write Problem & the Outbox Pattern. Flag it now: the moment you write to two systems, you have a consistency problem.
The thread
Section titled “The thread”What does this buy us, and what does it cost? Events buy decoupling (add consumers freely), resilience (a down consumer doesn’t block the producer), and load absorption (the log buffers spikes). They cost eventual consistency, harder debugging, and the dual-write hazard. The core mental shift: model your domain as a stream of immutable facts, and let interested parties decide what to do with them.
The architect’s lens
Section titled “The architect’s lens”Event-driven architecture is powerful and over-applied — run it through the five questions before you invert your whole system:
- Why does it exist? Because in a request-driven system every caller must know who to call (“ChargeCard”), tightly binding caller to callee. Events invert that — a service announces a fact (“OrderPlaced”) and stops knowing or caring who reacts.
- What problem does it solve? Decoupling and extensibility: add a new consumer (a loyalty-points service) and you change nothing in the producer. You also gain resilience (a down consumer doesn’t block the producer) and load absorption (the log buffers spikes).
- What are the trade-offs? You trade simplicity for decoupling: eventual consistency, no easy end-to-end tracing, and “what happened?” becomes stitching logs across services. Choreography leaves the flow emergent and hard to trace; orchestration re-centralizes visibility at the cost of a coordinator.
- When should I avoid it? For a small system with a few synchronous calls — events are a bad trade there. Reach for them only with many independent reactors or a need to absorb spikes, not by default; and remember event sourcing adds storage and replay cost.
- What breaks if I wire it naively? The dual-write problem: updating your database and publishing an event aren’t atomic, so a crash between them leaves state diverged (the order exists but no one was told). The fix is the transactional outbox — flag it the moment you write to two systems.
Check your understanding
Section titled “Check your understanding”- Distinguish a command from an event in terms of intent and coupling.
- Why can you add a new consumer to an event-driven system without touching the producer?
- Contrast choreography and orchestration; when is each appropriate?
- What does event sourcing store as the source of truth, and what problem do snapshots solve?
- Explain the dual-write problem and why reordering the two writes doesn’t fix it.
Show answers
- A command (“ChargeCard”) expresses intent — do this — directed at one service expected to act, so the caller knows the callee (tight coupling). An event (“OrderPlaced”) is a fact — this happened — broadcast to zero, one, or many consumers, so the caller knows no one. That shift from commands to events is exactly what decouples producer from consumers.
- Because the producer publishes once and knows nothing about who consumes. Consumers subscribe to the event log themselves, so adding a new one (say a loyalty-points service) means changing nothing in the producer — that extensibility is the headline benefit.
- In choreography each service reacts to events independently (loose coupling, but the flow is emergent and hard to trace); in orchestration a central coordinator directs each step (tighter coupling, but the whole saga lives in one visible place). Choreography fits simple, stable reactions; orchestration fits complex multi-step workflows needing rollback — you re-centralize visibility at the cost of a coordinator.
- Event sourcing stores the sequence of events as the source of truth (e.g.
[+100 deposited, -20 withdrawn]) instead of just current state, deriving state by replaying them. Snapshots solve the replay-cost problem: they let you start from a saved state so you don’t replay millions of events every time. - The dual-write problem: a service must update its database and publish an event, but those are two separate systems that can’t be made atomic — a crash after the DB write but before the publish leaves the order existing with no one told, so state diverges. Reordering doesn’t fix it because publish-first just moves the hazard — now the DB write might fail after the event went out. The robust fix is the transactional outbox.