Event-driven architecture
Event sourcing, CQRS, pub/sub patterns.
Stop thinking in CRUD ("update this row"). Start thinking in events ("this thing happened, who cares?"). Event-driven architecture turns one-to-one calls between services into one-to-many broadcasts, and unlocks event sourcing and CQRS along the way.
The big idea
A service does something, then emits an event describing what happened. Other services subscribe to events they care about. Nobody calls anybody.
The producer doesn't know — and doesn't need to know — who consumes the event.
Events vs commands
A naming discipline that pays back forever:
| Attribute | Command | Event |
|---|---|---|
| Tense | Imperative — "Charge the card" | Past — "Card was charged" |
| Direction | Sender → one specific receiver | Producer → anyone listening |
| Can be rejected? | Yes | No — it already happened |
| Example name | `ChargeCard`, `SendEmail` | `order.placed`, `card.charged` |
When a service should be told to do something, that's a command (queue + worker). When a service should react to something that happened elsewhere, that's an event (pub/sub).
What an event looks like
{
"id": "evt_9001",
"type": "order.placed",
"version": 1,
"occurred_at": "2025-11-12T08:00:00Z",
"actor": { "type": "user", "id": 42 },
"subject": { "type": "order", "id": 9001 },
"data": {
"total_cents": 4995,
"items": [{ "sku": "X1", "qty": 2 }]
}
}A few non-obvious rules:
- Version your event schema — you will need to evolve it.
occurred_at≠processed_at— keep both if order matters.- Include the subject ID, not the whole object — the subject can change after the event.
Event sourcing
Instead of storing current state, store the stream of events that produced it. Current state is derived by replaying the events.
order_9001 events:
order.created qty=2 sku=X1
order.item_added qty=1 sku=Y2
order.paid amount=4995
order.shipped tracking=…
state = reduce(events, applyEvent, {}) // = current orderWins:
- Audit log for free — the events are the history.
- Time travel — what was state at noon yesterday? Replay events up to then.
- New projections — need a new read view? Re-derive it from the event stream.
Cost: querying current state requires either replay (slow) or maintaining projections (extra code).
CQRS — separate the write and read models
Command Query Responsibility Segregation: writes go to one model (the event store / write side), reads come from a different model (denormalised, query-optimised).
The write side enforces invariants and emits events. The read side is whatever shape your queries need — a fast denormalised table, an ElasticSearch index, a graph database. Eventual consistency between them is the cost.
Pitfalls
When to reach for it
Event-driven is great when:
- One action triggers many downstream side effects (send email, update search index, update analytics, notify mobile).
- The downstream consumers change frequently (you'd otherwise be editing the producer every time).
- You need an audit log / replay capability anyway.
Event-driven is overkill for:
- A small app with a single team and tight coupling between modules.
- Anywhere you need strict global ordering or distributed transactions.
In practice
Start with events for integration between services (event bus between bounded contexts) before reaching for full event sourcing within a service. The first is a modest architectural choice; the second is a fundamental data-modeling commitment.
Key takeaways
- Events are facts in the past tense; commands are requests in the imperative. Name them right.
- A producer emits; many consumers subscribe — the producer doesn't know who.
- Event sourcing stores the history; current state is derived. Audit and time-travel for free.
- CQRS splits write and read models — accept eventual consistency, gain query freedom.
- Govern event ownership, version schemas, make subscribers idempotent.
- Reach for events to decouple consumers; reach for full event sourcing only with a clear need.
Checkpoint questions
Use these to test whether the lesson is clear enough to explain without rereading.
- 1What is the difference between an event and a command?
- 2When does event-driven design reduce coupling, and when does it create event spaghetti?
- 3Why do at-least-once delivery systems require idempotent handlers?
- 4How do event sourcing and CQRS change the read and write models?
References
External resources for going deeper after the lesson above.