Event-Driven Architecture
Event-driven architecture (EDA) builds systems that communicate by emitting and reacting to events — facts about something that happened — rather than only through synchronous request/response. Producers and consumers decouple in time and space, enabling fan-out, resilience to consumer downtime, and independent scaling.
EDA is powerful and easy to misuse. Adopt it when requirements need decoupling, fan-out, or durable integration — not because "async" sounds modern. Pair with System Design → Messaging for broker technology details.
| Message | Intent | Naming |
|---|---|---|
| Event | Something happened (fact) | OrderPlaced, PaymentCaptured |
| Command | Do this work (intent) | CapturePayment, ShipOrder |
| Query | Return data | GetOrder, SearchProducts |
Events are past tense and should not tell consumers how to react. Commands target a specific owner capable of performing an action. Confusing them creates hidden coupling (events that are really RPCs).
best practice
| Requirement | EDA fit |
|---|---|
| Many consumers react to one business fact | Strong — pub/sub fan-out |
| Producers must not wait on consumers | Strong — temporal decoupling |
| Audit/integration stream for other systems | Strong — durable log |
| User needs immediate strongly consistent read of all projections | Weak — sync may be simpler |
| Simple CRUD single app | Usually overkill |
- Why: reduce coupling between bounded contexts; absorb load spikes; integrate SaaS and analytics without blocking core paths.
- How: define domain events at module/service boundaries; publish via outbox; consume idempotently; document consistency lags in UX.
Queue (competing consumers)
Commands/work distribution; each message processed by one worker.
Pub/sub topics
Events to many independent subscribers.
Log/stream (e.g., Kafka-style)
Durable ordered log; replay; multiple consumer groups.
Choose based on retention, ordering, replay, and throughput NFRs. The broker is an adapter in hexagonal terms — domain should not be stuffed with vendor client code.
Consumers update their own state asynchronously. The system converges over time. Product and UX must acknowledge this: show pending states, avoid assuming immediate cross-service reads, and define reconciliation processes.
| Challenge | Practice |
|---|---|
| Duplicate delivery | Idempotent consumers; dedupe keys |
| Out-of-order | Version vectors; ignore stale; per-key ordering |
| Lost messages | Outbox + durable broker; monitoring lag |
| Poison messages | DLQ + alerting + replay tooling |
warning
| Approach | How it works | Best when |
|---|---|---|
| Choreography | Services react to each other's events | Simple fan-out; few steps |
| Orchestration | A coordinator sends commands/steps | Complex workflows; visibility needed |
Choreography can become a "spiderweb" that is hard to reason about. Orchestrators (workflow engines) centralize state machines but add a critical component. Choose deliberately and document the saga in an ADR.
- Start with in-process domain events inside a modular monolith.
- Add outbox + broker when crossing process boundaries.
- Standardize event envelope (id, type, time, correlation, payload version).
- Build consumer lag dashboards and DLQ runbooks before wide adoption.
- Version events compatibility-first; never silently reuse event type names.
| 1 | {{ |
| 2 | "id": "01HZX...", |
| 3 | "type": "order.placed.v1", |
| 4 | "time": "2026-07-30T12:00:00Z", |
| 5 | "source": "checkout", |
| 6 | "correlationId": "req_123", |
| 7 | "data": {{ "orderId": "ord_9", "total": 4200, "currency": "USD" }} |
| 8 | }} |
- Using events as synchronous RPC with hidden request/reply coupling.
- Shared database plus events — dual source of truth without rules.
- No idempotency — random double charges under retry.
- Unbounded event payloads with PII and no retention policy.
- Choreography with no tracing — impossible incident response.
| 1 | [ ] Requirements cite decoupling/fan-out/integration needs |
| 2 | [ ] Events vs commands distinguished |
| 3 | [ ] Consistency UX agreed with product |
| 4 | [ ] Outbox/inbox or equivalent reliability |
| 5 | [ ] Idempotent consumers + DLQ |
| 6 | [ ] Schema/version strategy |
| 7 | [ ] Tracing correlation IDs end-to-end |
| 8 | [ ] ADR written |
- Unit-test producers: assert event payloads for given domain actions.
- Consumer contract tests: sample events from schema registry/fixtures.
- Idempotency tests: deliver same event twice.
- Ordering tests where per-key order is promised.
- End-to-end journey tests with embedded broker in CI for critical paths.
Monitor consumer lag, DLQ depth, poison rates, and publish failures. Alert on lag SLO breaches. Practice replay drills. Treat schema changes like API changes — compatibility checks in CI.
| Ops concern | Control |
|---|---|
| Lag spike | Autoscale consumers; backpressure; check slow handlers |
| DLQ growth | Runbook; quarantine; fix; replay |
| Schema break | Compat tests; expand/contract versions |
| 1 | Checkout publishes OrderPlaced |
| 2 | → Inventory reserves stock |
| 3 | → Notify emails receipt |
| 4 | → Analytics updates funnel |
| 5 | → Fraud scores order asynchronously |
| 6 | Payments publishes PaymentCaptured / PaymentFailed |
| 7 | → Checkout updates order status |
| 8 | → Notify accordingly |
Treat events as public APIs. Prefer additive changes; use explicit versions in type names (order.placed.v2); run compatibility checks in CI; give consumers time to adopt (expand/contract).
| Change type | Safe approach |
|---|---|
| Add optional field | Usually compatible |
| Remove field | Deprecate first; wait for consumers |
| Change meaning of field | New event version — never silent |
| Split event | Publish both during transition |
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.