Event-Driven Architecture: Patterns for Real-Time Systems
March 5, 2024
Why event-driven
Event-driven architecture decouples producers from consumers. When a user places an order, the order service emits an event. The inventory service, notification service, and analytics service each consume that event independently. No direct coupling, no cascading failures.
The event bus
The event bus is the backbone of any event-driven system. We chose Redis Streams over Kafka for our use case because:
- Lower operational complexity (we already ran Redis)
- Sub-millisecond latency for most operations
- Built-in consumer groups for load balancing
- No JVM dependency
Event sourcing
Instead of storing the current state, store every event that led to that state. The current state is derived by replaying events. This gives you a complete audit trail and the ability to reconstruct state at any point in time.
CQRS (Command Query Responsibility Segregation)
Separate the write model from the read model. Commands go through the event-sourced write path. Queries read from optimized materialized views:
// Command side
type CreateOrderCommand struct {
UserID string
Items []OrderItem
Total Money
}
// Event
type OrderCreated struct {
OrderID string
UserID string
Items []OrderItem
Total Money
CreatedAt time.Time
}
// Query side (materialized view)
type OrderSummary struct {
OrderID string
Status string
ItemCount int
Total float64
CreatedAt time.Time
}Handling 2M+ events per minute
At this scale, every microsecond matters:
- Batch writes to the event store
- Use protobuf for serialization (smaller payloads, faster parsing)
- Partition by event type for parallel processing
- Implement backpressure at every consumer
- Monitor consumer lag as a first-class alert
Lessons learned
- Start with a simple event bus, not a complex event platform
- Schema evolution is harder than you think — use schema registry from day one
- Idempotent consumers are worth the extra effort
- Dead letter queues are not optional