Event-Driven Architecture: Messaging Patterns, Event Sourcing, and CQRS
Learn event-driven architecture patterns including publish/subscribe, event sourcing, CQRS, and how to build loosely coupled systems that scale.
Why Event-Driven?
In traditional request-response architectures, services call each other directly. This creates tight coupling and makes it hard to add new features without modifying existing services. Event-driven architecture solves this by having services communicate through events rather than direct calls.
Key insight: Events are facts that something happened. Commands are requests to do something. Understanding this distinction is crucial for event-driven design.
Core Concepts
Events vs Commands vs Queries
| Type | Direction | Response | Idempotent |
|---|---|---|---|
| Event | Publisher → Subscribers | None (fire-and-forget) | Yes, typically |
| Command | Client → Service | Ack/Nack | Usually |
| Query | Client → Service | Data | Yes |
Event Structure
{
"eventId": "uuid",
"eventType": "OrderPlaced",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"orderId": "12345",
"customerId": "67890",
"total": 99.99
}
}
Publish/Subscribe Pattern
How It Works
Use Cases
Benefits
- Decoupling: Publishers don't know who consumes their events
- Scalability: Add new consumers without changing producers
- Resilience: If a consumer is down, events are queued
- Audit trail: Events are a natural log of what happened
Event Streaming with Apache Kafka
Kafka is the backbone of many event-driven systems. It durably stores events and allows multiple consumers to process them independently.
Kafka Architecture
Kafka Concepts
| Concept | Description |
|---|---|
| Topic | A named stream of events |
| Partition | Ordered, immutable sequence within a topic |
| Offset | Position of a message in a partition |
| Consumer Group | Set of consumers that share partitions |
| Retention | How long messages are kept (configurable) |
Why Kafka over queues? Kafka maintains message order within partitions, supports message replay (you can re-read from the beginning), and handles high throughput (millions of messages/second).
Event Sourcing
Instead of storing current state, store the sequence of events that led to that state. The current state is derived by replaying events.
Traditional vs Event Sourcing
Event Store
-- Instead of updating a row, append events
CREATE TABLE events (
id UUID PRIMARY KEY,
aggregate_id UUID NOT NULL,
event_type VARCHAR(100) NOT NULL,
event_data JSONB NOT NULL,
created_at TIMESTAMP NOT NULL
);
-- Query events to reconstruct state
SELECT * FROM events
WHERE aggregate_id = 'order-123'
ORDER BY created_at;
Benefits of Event Sourcing
- Complete audit trail: You know exactly how state evolved
- Time travel: Query state at any point in time
- Replay and debug: Replay events to reproduce bugs
- Multiple projections: Create different read models from the same events
- Integration: Other systems can consume the same event stream
Challenges
| Challenge | Solution |
|---|---|
| Event schema changes | Upcasting, versioning |
| Large event stores | Snapshots, archiving old events |
| eventual consistency | Show loading states, be clear about latency |
| Debugging | Correlation IDs, structured logging |
CQRS (Command Query Responsibility Segregation)
Separate read and write operations into different models. Write side handles commands (changes state), read side handles queries (reads state).
Traditional vs CQRS
Example: E-Commerce
Benefits
- Optimize reads and writes independently: Different storage for different purposes
- Scalability: Scale read and write sides separately
- Flexibility: Read models can be tailored to specific UI needs
- Separation of concerns: Simpler command and query logic
CQRS is complex: Only use it when you have a genuine need (different read/write patterns, high scalability requirements). For most applications, a single model is simpler and sufficient.
Combining Event Sourcing + CQRS
This is a powerful combination used by systems like Event Store and Axon Framework.
Practical Implementation
Choosing an Event Broker
| Broker | Best For | Considerations |
|---|---|---|
| Kafka | High throughput, event streaming | Operational complexity |
| RabbitMQ | Traditional messaging, task queues | Single-node can be bottleneck |
| AWS EventBridge | AWS-native, serverless | Vendor lock-in |
| Google Pub/Sub | GCP-native, real-time | Vendor lock-in |
| NATS | Lightweight, high performance | Less ecosystem |
Event Design Best Practices
- Use past tense:
OrderPlaced,PaymentProcessed, notPlaceOrder - Include context: Enough data in the event to reconstruct state
- Version your events: Schema evolution is inevitable
- Be idempotent: Consumers should handle duplicate events
- Correlate events: Include correlation IDs for tracing
{
"eventType": "OrderPlaced",
"version": "2.0",
"correlationId": "req-12345",
"causationId": "cmd-67890",
"data": {
"orderId": "order-123",
"items": [...],
"customerId": "cust-456",
"totalAmount": 99.99,
"currency": "USD"
}
}
What to Remember for Interviews
- Events are facts: Past tense, immutable records of what happened
- Event sourcing: Store events, not state; replay to reconstruct
- CQRS: Separate read and write models for flexibility
- Decoupling: Event-driven enables independent service evolution
- Complexity: These patterns solve real problems but add operational complexity
Practice: Design an event-driven architecture for a ride-sharing app (like Uber). What events are published? What consumers need them? How would you handle a driver accepting a ride?