Event-Driven Architecture
Event-driven architecture connects parts of a system through events, enabling loose coupling and asynchronous workflows.
Core Idea
In an event-driven system, components communicate by producing and consuming events. An event represents something that happened — a fact about the past. Producers emit events without knowledge of who will consume them, and consumers react to events without knowledge of who produced them. This creates a fundamentally decoupled system.
Key Concepts
Events
An event is an immutable record of something that happened. Examples: OrderPlaced, PaymentReceived, InventoryReserved. Events describe facts, not commands — they represent the past tense of a business action.
Producers
Producers are components that emit events when something meaningful happens in their domain. A producer does not know or care who will consume its events.
Consumers
Consumers subscribe to events and react to them. A single event can be consumed by multiple consumers, each performing different actions.
Brokers
A message broker (such as Apache Kafka, RabbitMQ, or Azure Service Bus) sits between producers and consumers. It handles event routing, delivery guarantees, and buffering.
Eventual Consistency
Because events are processed asynchronously, different parts of the system may have temporarily inconsistent views of the world. The system converges to consistency over time, but at any given moment, some consumers may not yet have processed the latest events.
Benefits
- Loose coupling: Producers and consumers are independent; new consumers can be added without changing producers
- Temporal decoupling: Producers and consumers do not need to be available at the same time
- Scalability: Consumers can be scaled independently based on their processing needs
- Resilience: If a consumer is temporarily unavailable, events are buffered and processed when it recovers
- Audit trail: Events naturally form a log of everything that happened in the system
- Extensibility: New functionality can be added by introducing new consumers without modifying existing code
Trade-offs
- Debugging complexity: Following a workflow across asynchronous event flows is harder than tracing synchronous calls
- Eventual consistency: Users may see stale data; the system must be designed to handle this gracefully
- Ordering challenges: Guaranteeing event order across partitions or topics requires careful design
- Idempotency requirement: Consumers may receive the same event multiple times and must handle duplicates
- Schema evolution: Changing event formats while maintaining backward compatibility requires discipline
- Testing complexity: Testing asynchronous event flows is harder than testing request-response interactions
Common Failure Modes
- Lost events: Events that are produced but never persisted (the dual-write problem)
- Poison messages: Events that consistently fail processing and block the consumer
- Unbounded queues: Consumers that fall behind, causing memory pressure or data loss
- Event storms: Cascading events that amplify load across the system
- Zombie events: Events from decommissioned producers that no consumer expects
- Hidden coupling: Consumers that depend on the internal structure of events they should not know about
Related Patterns
Transactional Outbox
Solves the dual-write problem by writing events to an outbox table in the same database transaction as the business data. A separate process reads the outbox and publishes events to the broker.
Saga
Coordinates a multi-step business process across services using a sequence of events and compensating actions. If one step fails, previous steps are rolled back through compensation events.
Idempotent Consumer
A consumer that can safely process the same event multiple times without producing incorrect results. Typically implemented by tracking processed event IDs.
Dead-Letter Queue
A queue where events are moved after they fail processing repeatedly. Allows the system to continue processing other events while failed ones are investigated separately.