Event-Driven Architecture Patterns Explained
In a traditional request-response architecture, services call each other directly. Service A needs something from Service B, so it makes a synchronous call and waits. This works fine when you have a few services, but it creates tight coupling and cascading failures as your system grows.
Event-driven architecture (EDA) offers an alternative: services communicate by emitting and consuming events — immutable records of things that happened.
What Is an Event?
An event is a fact about something that occurred in your system:
{
"type": "order.placed",
"timestamp": "2025-12-28T10:00:00Z",
"data": {
"orderId": "abc-123",
"userId": "user-456",
"total": 49.99,
"items": [{ "sku": "WIDGET-01", "quantity": 2 }]
}
}
Events are facts, not commands. “Order was placed” is an event. “Process this order” is a command. This distinction matters because facts are observable by anyone who’s interested, while commands are directed at a specific handler.
Core Patterns
Event Notification
The simplest pattern. A service emits an event to notify others that something happened, without including much data. Consumers who care about the event fetch additional data they need from the source service.
Order Service --[order.placed (orderId)]--> Message Queue
|
+----+----+
| |
Email Service Analytics Service
(fetches order (fetches order
details) details)
Event-Carried State Transfer
The event includes enough data that consumers don’t need to call back to the source service. This reduces coupling and network traffic, but increases event size and creates data duplication.
Order Service --[order.placed (full order data)]--> Message Queue
Event Sourcing
Instead of storing current state, you store the sequence of events that led to the current state. The current state is derived by replaying events:
Events:
account.created {balance: 0}
account.deposited {amount: 100}
account.withdrew {amount: 30}
account.deposited {amount: 50}
Current state: balance = 120
This gives you a complete audit trail and the ability to reconstruct state at any point in time. The tradeoff is complexity — you need event storage, replay mechanisms, and snapshot strategies for performance.
CQRS (Command Query Responsibility Segregation)
Often paired with event sourcing. Separate your read model from your write model:
- Write side: Processes commands, emits events, maintains the event log
- Read side: Subscribes to events, builds optimized read models (denormalized tables, search indexes, caches)
| Write Model | Read Model | |
|---|---|---|
| Optimized for | Consistency, validation | Query performance |
| Data shape | Normalized | Denormalized |
| Storage | Event log | Any (SQL, NoSQL, search) |
| Updates | Through commands | Through event handlers |
Event Brokers
The infrastructure that routes events from producers to consumers:
- Apache Kafka: High-throughput, durable log. The dominant choice for event streaming at scale.
- RabbitMQ: Traditional message broker with flexible routing. Good for task queues and event notification.
- AWS EventBridge / SNS + SQS: Managed options on AWS. Less operational overhead.
- Redis Streams: Lightweight option for simpler use cases.
Benefits
- Loose coupling: Producers don’t know (or care) who consumes their events
- Scalability: Consumers can be added or removed without affecting producers
- Resilience: If a consumer is down, events queue up and are processed when it recovers
- Auditability: Every state change is recorded as an event
- Temporal decoupling: Producers and consumers don’t need to be available at the same time
Challenges
- Eventual consistency: State updates propagate asynchronously. Your system is eventually consistent, not immediately consistent.
- Ordering: Events may arrive out of order, especially across partitions. You need strategies to handle this (timestamps, sequence numbers, idempotent consumers).
- Schema evolution: Events are stored permanently. When you change the schema, old events still exist. Version your events and plan for backward compatibility.
- Debugging: Tracing a request through an event-driven system is harder than tracing a synchronous call chain. Distributed tracing and correlation IDs are essential.
Event-driven systems trade simplicity for scalability and resilience. Don’t adopt EDA because it’s interesting — adopt it because you have a specific problem it solves: high throughput, loose coupling, or temporal decoupling.
When to Use EDA
- High-throughput event processing: Real-time analytics, audit logging, data pipelines
- Loosely coupled services: When you want services to evolve independently
- Asynchronous workflows: Order processing, notifications, background jobs
- Event replay: When you need to rebuild state or reprocess historical data
When Not to Use EDA
- Simple request-response flows: A CRUD app doesn’t need event sourcing
- Strong consistency requirements: If you need immediate consistency across services, synchronous communication may be more appropriate
- Small teams: The operational overhead of event infrastructure may not be justified
Event-driven architecture is a powerful tool, but it introduces complexity that must be managed deliberately. Start with simpler patterns (event notification) and adopt more complex ones (event sourcing, CQRS) only when the requirements demand it.