Event-Driven Architecture
Event-Driven Architecture (EDA) is a style where components communicate by publishing events and reacting to events rather than calling each other directly. A producer announces that something happened and doesn't know or care who consumes it. This decouples systems, scales workflows independently, and makes integrations resilient — at the cost of eventual consistency and the operational care that asynchronous systems demand.
The mental shift: events are facts about the past ("UserCreated"), broadcast to whoever's interested. That's powerful for fan-out and integration, but it's the wrong tool when you need an immediate, tightly-coupled request/response — use synchronous calls there.
TL;DR
- Events represent "something that happened" in the past.
- Prefer events for fan-out and integration; sync calls for tight request/response.
- Most systems are at-least-once — build idempotent consumers.
- Expect eventual consistency and design UX/ops around it.
Quick Example
A well-formed event carries identity, type, time, version, and payload:
Events vs Commands
- Event — a fact:
UserCreated,PaymentCaptured,PostPublished. Broadcast; may have many consumers. - Command — an instruction:
CreateUser,CapturePayment. Addressed to a specific handler.
The distinction matters: events describe what happened (past tense, no expectation of who acts); commands request that something happen (imperative, one owner).
Brokers & Delivery Semantics
Events flow through message queues, event streams, or pub/sub systems. Crucially, most real systems guarantee at-least-once delivery:
⚠️ Duplicates will happen. Consumers must be idempotent — processing the same event twice must be harmless.
See Message Queues.
Coordination Patterns
- Choreography — services react to events with no central coordinator. Highly decoupled, but the global flow is hard to see and emergent complexity creeps in.
- Orchestration — a workflow service coordinates steps. Clearer control flow, but the coordinator becomes a critical path.
- Saga — a long-running transaction across services where each step has a compensating action to undo it; coordinated via events (choreography) or an orchestrator. The standard answer to "distributed transactions" without two-phase commit.
Event Design & Versioning
Include a unique id, type, timestamp, producer/version, and payload (see the Quick Example). Because events live a long time, evolve them in a backward-compatible way:
- Add fields freely.
- Avoid renaming or removing fields.
- Bump the version when you must make a breaking change.
CQRS & Event Sourcing
- CQRS (Command Query Responsibility Segregation) — separate the write path (commands) from the read path (queries), letting read models be optimized for how they're queried.
- Event sourcing — instead of storing current state, store the log of events and rebuild state from it. Gains: auditability and temporal queries. Costs: real complexity and schema-evolution challenges.
💡 Many teams adopt "event-driven" for integration and decoupling without full event sourcing — you don't need one to get the other.
Reliability
- Idempotency — store processed event ids, use natural keys, or do conditional writes so duplicates are no-ops.
- Outbox pattern — to publish reliably when writing to a database, write the state change and an outbox row in one transaction, then a separate publisher reads the outbox and emits the event. This avoids the "saved to DB but failed to publish" inconsistency.
- Observability — track publish failures, consumer lag, dead-letter-queue (DLQ) rate, and end-to-end workflow latency. See Logging.
Best Practices
- Make consumers idempotent — non-negotiable under at-least-once delivery.
- Use the outbox pattern to publish atomically with your DB writes.
- Version events and evolve them backward-compatibly.
- Bound retries and route exhausted messages to a DLQ.
- Own event contracts explicitly — schemas need governance like APIs do.
Common Mistakes
Treating events like RPC
Missing idempotency
FAQ
When should I use events vs direct (synchronous) calls?
Use events when work can happen asynchronously, multiple consumers care, or you want to decouple producer and consumer lifecycles — fan-out, integration, background processing. Use synchronous calls when the caller needs an immediate result to proceed (e.g. "is this user authorized right now?"). Forcing a request/response interaction through events couples them anyway while adding latency and complexity.
Why must event consumers be idempotent?
Because nearly all brokers guarantee at-least-once (not exactly-once) delivery — network retries and redeliveries mean a consumer can receive the same event more than once. If processing isn't idempotent, duplicates cause double-charges, duplicate records, or corrupted state. Dedupe on the event id or a natural business key so reprocessing the same event has no additional effect.
What is the outbox pattern and why do I need it?
It solves the dual-write problem: you need to both update your database and publish an event, but doing them separately risks one succeeding and the other failing. The outbox pattern writes the state change and an "outbox" event row in a single transaction; a separate process then reads the outbox and publishes. This guarantees the event is published if and only if the data was committed.
Do I need event sourcing to do event-driven architecture?
No. Event-driven architecture is about communicating via events; event sourcing is a specific persistence strategy (store events, derive state). You can publish and react to events while still storing current state in a normal database — most event-driven systems do exactly that. Event sourcing adds auditability and temporal queries but also significant complexity, so adopt it only when those benefits are worth it.
Related Topics
- Message Queues — The transport layer
- Microservices — Where EDA shines
- Domain-Driven Design — Domain events
- Design Patterns — Observer/pub-sub
- System Design — Async at scale