Event-Driven Architecture Explained: Patterns, Trade-offs, and When to Use It
Understand event-driven architecture with pub/sub, event sourcing, CQRS, and sagas—plus when async beats request/response.
Event-driven architecture (EDA) organizes systems around events—facts that something happened—instead of direct request/response calls. When a user completes checkout, you publish an OrderPlaced event; inventory, email, and analytics services react without the checkout service knowing they exist.
Quick answer: use event-driven design when you need loose coupling, independent scaling, or near-real-time reactions across many services. Stick with synchronous APIs when you need immediate consistency, simple debugging, or a small monolith. Most production systems blend both.
Events vs commands vs queries
| Concept | Meaning | Example |
|---|---|---|
| Event | Past tense, immutable fact | PaymentCaptured |
| Command | Request to do something | ChargeCustomer |
| Query | Read without side effects | GetOrderStatus |
EDA focuses on broadcasting events. Commands often travel point-to-point (HTTP, gRPC); events flow through brokers (Kafka, SNS, RabbitMQ, Azure Event Grid).
Core building blocks
Event producers
Services emit events after state changes succeed. The producer should not wait for all downstream consumers—fire-and-forget to the broker (with durability guarantees).
Event broker / bus
Middleware that stores and routes events:
- Apache Kafka — high-throughput log, replayable
- AWS SNS + SQS — managed fan-out
- NATS, RabbitMQ — flexible routing patterns
Event consumers
Subscribers process events idempotently. Multiple consumers can react to the same event independently (email + warehouse + BI).
Common EDA patterns
1. Publish/Subscribe (Pub/Sub)
One publisher, many subscribers. New features subscribe without changing the producer.
Good for: notifications, cache invalidation, audit trails.
2. Event notification
Minimal payload—consumers fetch details via API if needed. Reduces coupling but adds latency on the consumer side.
3. Event-carried state transfer
Event includes all data subscribers need (e.g., full order snapshot). Faster consumers, larger payloads, harder schema evolution.
4. Event sourcing
Store state as a sequence of events instead of overwriting rows. Current state = replay all events (often with snapshots).
Benefits: complete audit history, temporal queries ("what did balance look like on Tuesday?").
Costs: complexity, eventual consistency on reads, migration pain.
5. CQRS (Command Query Responsibility Segregation)
Separate write models (commands → events) from read models (optimized projections). Works well with event sourcing but can stand alone.
Example: writes go to an order service; a projection builds a denormalized OrderSummary table for fast dashboards.
6. Sagas (distributed transactions)
Multi-step workflows across services without two-phase commit. Each step publishes an event; compensating events undo prior steps on failure.
Order saga: reserve inventory → charge payment → ship. If payment fails, publish InventoryReleaseRequested.
Implement as choreography (services listen and react) or orchestration (central coordinator).
Benefits of event-driven architecture
Loose coupling — Producers do not need consumer endpoint lists.
Elastic scale — Scale consumers per event type (100 email workers, 10 fraud checks).
Extensibility — Add analytics by subscribing a new consumer; zero changes to checkout.
Resilience — Queues buffer spikes; retries handle transient failures.
Trade-offs and challenges
Eventual consistency — Users may see stale reads briefly. UI copy and product design must account for delays.
Debugging difficulty — A single user action spans async traces. Invest in correlation IDs propagated in every event envelope:
{
"eventId": "uuid",
"correlationId": "request-abc",
"type": "OrderPlaced",
"occurredAt": "2026-09-15T12:00:00Z",
"payload": { "orderId": "123" }
}
Schema evolution — Consumers break if you rename fields carelessly. Use versioned schemas (JSON Schema, Avro, Protobuf) and compatibility rules.
Ordering — Global order is expensive. Partition by key (customer ID, order ID) when order matters within a slice only.
Duplication — At-least-once delivery means idempotent handlers are mandatory.
When EDA is the wrong default
Skip full EDA when:
- Team maintains a single deployable monolith under ~10 engineers
- Business rules require strong synchronous consistency (e.g., real-time trading ledger)
- Operational maturity for Kafka/broker tuning is not there yet
Start synchronous; extract events at bounded contexts when pain appears (deployment coupling, scaling bottlenecks).
Migration path from monolith
- Identify domain events already implied in code (
OrderCreatedafter DB commit). - Outbox pattern — Write events to an outbox table in the same DB transaction as business data; a relay publishes to the broker. Avoids dual-write inconsistency.
- Strangle read paths — Build projections for hot queries while writes stay in monolith.
- Extract one consumer service with clear boundaries (e.g., notifications).
Technology selection hints
| Requirement | Often choose |
|---|---|
| Replay + high volume | Kafka |
| AWS-native, ops-light | SNS/SQS or EventBridge |
| Complex routing | RabbitMQ |
| Multi-cloud abstraction | Managed Kafka (Confluent, MSK) |
Testing event-driven systems
- Contract tests between producer schemas and consumers (Pact, schema registry checks in CI)
- In-memory broker fakes for unit tests
- Ephemeral environments with recorded production traffic samples (sanitized)
FAQ
Is EDA the same as microservices?
No. You can run events inside a monolith (in-process event bus). Microservices often use EDA but do not require it.
How do I handle failures?
Dead-letter queues, exponential backoff, alerting on DLQ depth, and manual replay tools.
What about GDPR deletes?
Event sourcing complicates erasure. Plan tombstone events, retention limits, or avoid sourcing PII-heavy entities.
Sync API + async events together?
Common pattern: HTTP returns 202 Accepted with a resource ID; client polls or receives WebSocket push when processing completes.
Real-world example: e-commerce order flow
Walk through a simplified checkout to see events in motion:
- Checkout service persists order, emits
OrderPlacedwith{ orderId, customerId, total }. - Inventory service consumes, reserves stock, emits
InventoryReservedorInventoryRejected. - Payment service listens for
InventoryReserved, charges card, emitsPaymentCapturedorPaymentFailed. - Email service sends confirmation on
PaymentCaptured. - Analytics projector updates a read model for the admin dashboard.
No service calls another synchronously except where UX demands it (payment authorization may still block the HTTP response). Failures in email never roll back payment—events reflect business reality.
Anti-patterns to avoid
Chatty synchronous chains disguised as events. Publishing an event only to trigger an immediate synchronous callback defeats the purpose.
God topics. One Kafka topic named events with fifty unrelated schemas becomes ungovernable. Partition by bounded context (orders, billing, notifications).
Missing consumer lag alerts. If inventory consumers fall six hours behind, you oversell. Monitor lag per consumer group.
PII in every event. Pass IDs, not full customer records. Fetch details from a secured store when needed.
Observability essentials
Adopt OpenTelemetry trace context injection into event headers. When support asks "what happened to order 8842?", one trace ID should link HTTP request → published events → consumer spans.
Log structured fields: eventType, eventId, correlationId, partitionKey. Avoid logging full payloads in production.
Choosing sync vs async at design time
Use this decision lens during design reviews:
| Favor synchronous HTTP/gRPC | Favor events |
|---|---|
| User waits for the result on screen | Side effects can happen seconds later |
| Strong consistency required now | Eventual consistency acceptable |
| Two services, small team | Many subscribers, evolving over time |
| Simple failure = show error immediately | Retries and DLQs are acceptable |
Hybrid is normal: the API returns the created resource ID synchronously while ResourceCreated triggers downstream indexing asynchronously.
Bottom line
Event-driven architecture trades immediate simplicity for long-term flexibility. Model your domain events explicitly, invest in observability and schema discipline, and adopt async flows where decoupling clearly reduces pain—not everywhere by default.
Comments
Loading comments…