Event-Driven Architecture: Patterns and Pitfalls
Events, Commands, and Queries
In EDA, an event is an immutable record of something that happened: OrderPlaced, UserRegistered, PaymentProcessed. Events are facts — they describe past state changes and cannot be rejected by consumers. Commands are requests to perform an action (PlaceOrder, RegisterUser) that may be accepted or rejected. Queries read current state without side effects. Keeping these three concepts distinct is fundamental to a clean event-driven design.
Name events in past tense (OrderPlaced, not OrderPlace or PlaceOrder). This reinforces that events are facts about things that already happened, not requests.
Publish-Subscribe Pattern
The simplest EDA pattern is publish-subscribe (pub/sub). A producer publishes events to a topic or channel. Multiple consumers subscribe to that topic and process events independently. Producers don't know about consumers, and adding new consumers doesn't require changing the producer. This pattern is ideal for fan-out scenarios like sending notifications, updating search indexes, and generating analytics — all triggered by a single business event.
interface OrderPlacedEvent {
type: "OrderPlaced"
orderId: string
customerId: string
items: OrderItem[]
total: number
occurredAt: string
}
// Producer: publishes after order is saved
await eventBus.publish("orders", orderPlacedEvent)
// Consumer 1: sends confirmation email
// Consumer 2: updates inventory counts
// Consumer 3: triggers fraud detection analysisEvent Sourcing
Event sourcing takes EDA further by making the event log the primary source of truth. Instead of storing current state in a database and losing history, you store the complete sequence of events that led to the current state. Current state is derived by replaying events from the beginning (or from a snapshot). This provides a complete audit trail, the ability to reconstruct state at any point in time, and natural support for temporal queries.
Event sourcing adds significant complexity. Use it only when the audit trail, temporal queries, or replay capabilities provide concrete business value — typically in financial systems, compliance-heavy domains, or collaborative editing.
Idempotency: The Non-Negotiable Requirement
In any distributed event system, messages can be delivered more than once due to network retries, consumer restarts, or broker redelivery. Every event consumer must be idempotent — processing the same event twice must produce the same result as processing it once. Common strategies include using a unique event ID and tracking processed IDs in a deduplication table, designing operations to be naturally idempotent (e.g., SET status = 'shipped' is idempotent, INCREMENT counter is not), and using database constraints to prevent duplicate side effects.
- Always include a unique eventId in every event payload
- Track processed event IDs in a deduplication store with TTL
- Prefer idempotent database operations (upserts over inserts)
- Test idempotency explicitly: publish the same event twice and verify correct behavior