Event-Driven Architecture
Communicating through immutable, persisted events — replayability, idempotency, event sourcing, CQRS, and the trade-offs of eventual consistency.
Event-driven systems communicate through events — records of state changes (“order placed”, “payment failed”) rather than direct commands.
Flow: Producer → Broker (queue/log) → Consumer(s)
Two properties make events powerful:
- Immutable — never updated, only appended.
- Persisted — stored durably, enabling replay and recovery.
Replayability
Because the event log is a complete history, system state can be rebuilt by reprocessing events from the beginning. This enables:
- Self-healing — after a bug corrupts state, fix the handler and replay the log.
- Debugging and auditing — every change has a traceable cause.
- New consumers — subscribe and replay history to build a current view.
Brokers like Kafka and RabbitMQ provide the persistence, retry, and delivery machinery.
Reliability Requirements
Idempotency
At-least-once delivery means the same event can arrive multiple times. Handlers must be idempotent — processing an event twice must be safe. Common technique: track processed event IDs and dedupe.
Retries + ACKs
Consumers acknowledge messages only after successful processing. Unacked messages are redelivered — this is what makes delivery reliable, and also why idempotency is non-negotiable.
Event Sourcing & CQRS
- Event Sourcing: store all changes as events, not final state. The database is the event log; current state is derived.
- CQRS (Command Query Responsibility Segregation): separate the write model (commands producing events) from the read model (projections built from those events). Each side scales and evolves independently.
Strengths
- Loose coupling — services evolve independently; add new consumers without touching producers.
- Scalability — asynchronous processing absorbs spikes naturally.
- Availability & fault tolerance — consumers can be down while events accumulate safely in the broker.
Weaknesses
- No strict ordering guarantee by default — depends on partitioning/design.
- Eventual consistency — reads may lag writes; wrong fit for strong transactional requirements.
- Distributed debugging — tracing one request across many async consumers is hard without correlation IDs.
- Careful handling required for retries, duplicates, ordering, and failure paths.
Real-World Uses
- Payments, notifications, analytics pipelines
- Git (the commit history is an event log)
- Gaming (rebuilding game state from event history)
Not ideal when you need strong transactional consistency across a write — use a relational design there instead.
Part of the Messaging series.
Related Notes
Message Queues: Async Processing at Scale
How message queues decouple producers from consumers, survive worker crashes with retries and reassignment, and form the basis of pub-sub and event-driven systems.
Microservices Architecture Patterns
Essential patterns for designing, deploying, and operating microservices: decomposition, communication, data management, and operational patterns.
CAP Theorem: Understanding the Trade-offs
A deep dive into the CAP theorem, its implications for distributed systems, and how to choose the right trade-offs for your system.