Event-Driven Architecture: Patterns, Pitfalls and Practical Advice
A practical guide to event-driven architecture — events vs commands, pub/sub, event notification vs event-carried state, sagas, event sourcing, CQRS, schema evolution and debugging.
In an event-driven architecture (EDA), components communicate by producing and reacting to events: facts about things that happened, like OrderPlaced, PaymentFailed or UserSignedUp. Instead of the order service calling billing, inventory, email and analytics directly, it announces "an order was placed", and each interested service reacts on its own.
Done well, EDA gives you loosely coupled, resilient, scalable systems. Done poorly, it gives you an invisible web of dependencies where nobody can explain why a customer got three emails.
Key takeaways
- Events describe facts in the past tense; commands request actions. Keep the distinction clear.
- EDA decouples producers from consumers and enables fan-out, resilience and independent scaling.
- Choose between event notification (thin events) and event-carried state transfer (fat events) deliberately.
- Multi-step business processes across services need sagas with compensating actions.
- Invest in schemas, idempotency, observability and ownership, or EDA becomes impossible to debug.
Events vs commands
| Event | Command | |
|---|---|---|
| Meaning | Something happened | Please do something |
| Naming | Past tense: OrderPlaced |
Imperative: ChargeCard |
| Sender knows receivers? | No | Yes (a specific handler) |
| Can be rejected? | No, it's a fact | Yes |
| Consumers | Zero to many | Exactly one |
Mixing them up, for example publishing SendWelcomeEmail as an "event", recreates tight coupling through a message broker.
Why go event-driven?
- Loose coupling. Producers don't know or wait for consumers. New consumers (a fraud check, a loyalty program) plug in without touching the producer.
- Resilience. If the email service is down, orders still succeed; emails go out when it recovers.
- Scalability. Consumers scale independently, and brokers absorb spikes (message queues explained).
- Real-time data flow. Analytics, search indexes and caches stay updated from the same event stream.
- Audit and replay. With a durable log, you can rebuild views or reprocess history (Apache Kafka explained).
Core patterns
Event notification (thin events)
json{"type": "OrderPlaced", "order_id": "ord_123", "occurred_at": "2026-09-28T10:00:00Z"}
Consumers call back to the order service for details.
- ✅ Small events, and producers don't expose their full data model.
- ⚠️ Consumers depend on the producer's API being available; there's more chatty traffic.
Event-carried state transfer (fat events)
json{"type": "OrderPlaced", "event_id": "evt_9f2", "order_id": "ord_123",
"customer_id": "cus_42", "items": [{"sku": "SKU-1", "qty": 2, "price": 19.99}],
"total": 39.98, "currency": "USD", "occurred_at": "2026-09-28T10:00:00Z", "version": 1}
Consumers have everything they need and can maintain local copies.
- ✅ True autonomy; consumers work even if the producer is down.
- ⚠️ Larger events, duplicated data, and a schema contract that must evolve carefully.
Publish/subscribe and event streaming
Brokers route events to subscribers. Log-based platforms retain events so new consumers can start from the beginning and existing consumers can replay.
Sagas: distributed transactions without distributed locks
A business process spans services, such as order → payment → inventory → shipping. You can't wrap them in one database transaction. A saga is a sequence of local transactions, each publishing an event, with compensating actions to undo prior steps when something fails.
textOrderPlaced ─▶ Payment: charge ─▶ PaymentSucceeded ─▶ Inventory: reserve ─▶ StockReserved ─▶ Shipping
│
StockUnavailable
▼
Payment: refund (compensation) ─▶ OrderCancelled
- Choreography: services react to each other's events. There's no central coordinator, and it's simple for short flows, but hard to see the whole process.
- Orchestration: a coordinator (a workflow engine or orchestrator service) tells each service what to do and tracks state. More visible and easier to change for complex flows.
Rule of thumb: choreography for two or three steps; orchestration beyond that.
Event sourcing
Instead of storing current state, store the sequence of events that led to it, and derive state by replaying them. It gives a perfect audit history and the ability to rebuild views, but it adds complexity (snapshots, schema evolution of historical events, the learning curve). Use it in domains where history is the product: ledgers, compliance-heavy workflows. It's rarely a good default.
CQRS
Command Query Responsibility Segregation separates the write model from read models. Events update denormalized read models optimized for queries (a search index, a dashboard table). CQRS pairs naturally with EDA but, like event sourcing, should be applied where read and write needs genuinely diverge.
Reliability essentials
- Transactional outbox: publish events reliably alongside database changes, avoiding dual writes (change data capture explained).
- Idempotent consumers: events will be delivered more than once. Deduplicate by
event_idor make effects idempotent. - Ordering per entity: partition by aggregate ID (such as
order_id) when order matters; handle out-of-order events with versions. - Dead-letter queues and redrive tooling for poison messages.
- Timeouts in sagas: what happens if a step never responds?
Schema design and evolution
Events are public contracts between teams:
- Include an
event_id,type,version,occurred_atand entity IDs. - Use a schema format and registry that enforces backward-compatible changes (add optional fields; don't remove or rename fields in place).
- Publish domain events ("OrderPlaced") rather than raw database row changes, so internal schema refactors don't break consumers.
- Document events in a catalog: owner, schema, semantics and consumers.
Observability: making the invisible visible
EDA's biggest operational risk is opacity. Build in:
- Correlation IDs propagated through every event, so you can trace a customer action across services (observability explained).
- Distributed tracing across async boundaries.
- Consumer lag and DLQ metrics per subscription.
- Business-level monitoring: "orders placed vs orders shipped within 24h". This catches stuck sagas that technical metrics miss.
When not to go event-driven
- Simple CRUD applications with one team: synchronous calls or a modular monolith are easier.
- When the caller needs an immediate answer (validating a coupon at checkout): use a synchronous request.
- When the team lacks the operational maturity to run brokers and debug async flows.
A healthy hybrid is common: synchronous APIs for queries and immediate decisions, events for side effects and data propagation.
FAQ
Is event-driven architecture the same as microservices?
No. Microservices are about deployment boundaries; EDA is about communication style. Microservices often use events, but you can use events inside a monolith (in-process domain events) too.
What's the difference between event-driven and message-driven?
Event-driven systems broadcast facts that zero or more consumers may react to. Message-driven systems send messages (often commands) to specific recipients. In practice, systems use both.
How do I debug event-driven systems?
Propagate correlation IDs, use distributed tracing across async hops, log event IDs at produce and consume time, maintain an event catalog, and monitor business-level flow metrics to spot stuck processes.
Do I need Kafka for event-driven architecture?
No. Kafka suits high-throughput streams with replay needs. Managed pub/sub services, RabbitMQ or even a database outbox with a job runner can support EDA at smaller scale.