Event-Driven Architecture
Building systems that react to facts — events, brokers, choreography, and the patterns that keep async systems sane
In an event-driven architecture (EDA), components communicate by announcing that something happened rather than by calling each other to make something happen. A producer publishes an event such as “order placed” or “payment captured” to a broker, and any number of consumers react on their own schedule without the producer knowing who they are. That inversion is the source of EDA’s decoupling, elasticity, and resilience, and also of its hardest problems: eventual consistency, duplicate and out-of-order delivery, and flows that no single service owns. This hub defines the core concepts (events versus commands, the kinds of event, choreography versus orchestration, and what asynchrony buys and costs) and links to the detailed pages on message brokers and event-driven patterns.
Request-Driven vs. Event-Driven
In a request-driven system, control flows by command: service A calls B and waits, B calls C and waits, and the chain is only as available as its weakest link. Event-driven architecture replaces the call chain with a stream of facts. Components publish immutable records of what happened, and other components subscribe to the ones they care about.
flowchart LR
subgraph Sync["Request-driven: a call chain"]
direction LR
O1["Order"] -->|"call, wait"| P1["Payment"]
P1 -->|"call, wait"| I1["Inventory"]
I1 -->|"call, wait"| E1["Email"]
end
subgraph Async["Event-driven: a stream of facts"]
direction LR
O2["Order"] -->|"OrderPlaced"| B[("Broker")]
B --> P2["Payment"]
B --> I2["Inventory"]
B --> E2["Email"]
end
In the call chain, an outage in Email fails the whole order and every added step adds latency. In the event-driven version, Order returns as soon as the event is durably stored, and a failed Email service only delays emails.
Events vs. Commands
The most important distinction in this area is between an event and a command. Both are messages, but they carry opposite intent, and confusing them is the most common way event-driven designs go wrong.
- A command is an imperative: do this. It is addressed to one handler, which owns the outcome and usually replies.
ChargeCard,SendEmail, andReserveInventoryare commands. The sender depends on the receiver existing. - An event is a notification: this happened. It is named in the past tense, addressed to no one, and expects no reply.
CardCharged,EmailSent, andInventoryReservedare events. Whether zero or fifty consumers react is not the emitter’s concern.
| Aspect | Command | Event |
|---|---|---|
| Intent | “Do this” | “This happened” |
| Naming | Imperative (ReserveSeat) |
Past tense (SeatReserved) |
| Recipients | Exactly one handler | Zero to many subscribers |
| Coupling | Sender knows the receiver | Emitter knows no one |
| Reply | Often expects a result | None |
| Schema owned by | The receiver (what it accepts) | The emitter (what it publishes) |
| Can be rejected? | Yes, by the handler | No; it already happened |
| Adding a consumer | Sender must route to it | Emitter is unchanged |
Commands keep coupling; events shed it. An “event” named in the imperative and aimed at one service, such as OrderShipmentRequested consumed only by shipping, is a command in disguise, and the disguise costs you the first time a second consumer appears. Both are legitimate: use commands where one party must act and own the result, and events where a fact should be broadcast. The discipline is naming each for what it is.
What an Event Carries
Martin Fowler distinguishes several patterns that are all called “event-driven” but make different trade-offs:
| Pattern | Event contains | Consumers | Trade-off |
|---|---|---|---|
| Event notification | A minimal signal: type, ID, timestamp | Call back to the source for details | Small, stable events; consumers re-couple through the callback |
| Event-carried state transfer | The changed state itself | Keep a local copy of the data they need | Consumers work when the source is down; data is duplicated and eventually consistent |
| Event sourcing | Every state change, as the system of record | Rebuild state by replaying events | Full history and audit trail; replay, versioning, and snapshots add complexity |
| CQRS | Events feed separate read models | Query-optimized projections | Reads and writes scale independently; read models lag the write side |
The first two concern messages between services; the last two concern how a service stores its own state. They combine freely. Event-Driven Patterns covers event sourcing, CQRS, and projections in depth.
Because events are an interface between teams, their shape is a public contract. Standard envelopes such as CloudEvents and a schema registry with compatibility rules keep producers and consumers evolving independently; see Event Versioning & Schema Registry.
Choreography vs. Orchestration
When a business process spans several services (place order, charge card, reserve stock, ship), someone has to drive the sequence. There are two answers.
Choreography has no conductor. Each service subscribes to the events it cares about, does its work, and emits its own event, which the next service listens for. The flow emerges from who listens to what. This maximizes decoupling and makes adding a reaction easy, but the end-to-end process exists only implicitly, spread across subscriptions, which makes it hard to see, monitor, and change.
Orchestration introduces a coordinator that holds the process definition explicitly. It sends commands, waits for replies, and decides what happens next, including compensating earlier steps when a later one fails. The flow lives in one place where it can be read and monitored, at the cost of a central component coupled to every step. Durable execution engines such as Temporal, Restate, and AWS Step Functions have made orchestration much cheaper to build correctly, because they persist the workflow’s progress and resume it after crashes; see Durable Execution.
sequenceDiagram
participant O as Order
participant C as Saga coordinator
participant P as Payment
participant W as Warehouse
Note over O,W: Orchestration, with a compensation on failure
O->>C: start PlaceOrder
C->>P: ChargeCard (command)
P-->>C: CardCharged
C->>W: ReserveStock (command)
W-->>C: OutOfStock
C->>P: RefundCard (compensation)
P-->>C: CardRefunded
C-->>O: OrderRejected
In the choreographed version of the same flow, Payment reacts to OrderPlaced, Warehouse reacts to CardCharged, and on failure Warehouse emits StockReservationFailed, to which Payment reacts by refunding. Every service must know which failure events concern it, which is manageable for two steps and hard to follow for ten.
| Choreography | Orchestration | |
|---|---|---|
| Control | Distributed; each service decides | Centralized in a coordinator |
| Messages | Events | Commands and replies |
| Coupling | Lowest; no service knows the whole flow | Coordinator coupled to every step |
| Visibility | Implicit; reconstructed from traces | Explicit; the workflow definition |
| Adding a step | Subscribe a new consumer | Change the coordinator |
| Failure handling | Each service compensates on failure events | Coordinator runs compensations |
| Best for | Independent side effects, few steps | Long-running, multi-step transactions with ordering and timeouts |
A useful rule of thumb is choreograph the simple, orchestrate the complex. Independent side effects (“when an order is placed, update analytics and send a receipt”) suit choreography. A distributed transaction with ordering, timeouts, and compensation, the saga, is usually clearer as an orchestration. Many systems use both: choreography between bounded contexts, orchestration for the critical path inside one.
Asynchronous Decoupling
The central benefit of event-driven design is temporal decoupling: producer and consumer need not be available at the same time. A synchronous caller is coupled to its callee in time, availability, and throughput. With a broker in between, the producer writes an event and returns; the broker holds it durably; the consumer processes it when ready, possibly after recovering from a crash.
flowchart LR
Prod["Producer<br/>(returns immediately)"] --> Q[("Broker or log<br/>durable buffer")]
Q --> C1["Consumer A<br/>(keeping up)"]
Q --> C2["Consumer B<br/>(was offline, catching up)"]
Q --> C3["Consumer C<br/>(new, replaying history)"]
That change yields several properties:
- Load leveling. A traffic spike lengthens the queue instead of overwhelming the downstream service, which drains it at a sustainable rate.
- Failure isolation. A crashed consumer does not affect the producer; events accumulate until it returns.
- Independent scaling. Producers and consumers scale separately, and consumers can autoscale on backlog.
- Cheap fan-out. New consumers attach without any change to the producer.
- Replay. Log-based brokers retain events, so a new or fixed consumer can reprocess history to build or rebuild a view.
These gains have costs:
| Cost | Consequence | Mitigation |
|---|---|---|
| Eventual consistency | The order exists before the invoice does; reads may be stale. | Design read paths and UIs to tolerate lag; show pending states. |
| Duplicate delivery | At-least-once delivery means the same event arrives more than once. | Idempotent consumers. |
| Reordering | Events for different keys interleave; retries and redelivery reorder. | Key by entity ID; carry version numbers and discard stale updates. |
| Dual writes | Updating a database and publishing an event are two operations that can partially fail. | The transactional outbox or change data capture. |
| Lost traceability | One user action becomes a cascade of events with no stack trace. | Correlation IDs and distributed tracing with context propagated in message headers (Observability). |
| Poison messages | A message that always fails blocks its queue or partition. | Bounded retries and dead-letter queues. |
When Event-Driven Is the Right Choice
Event-driven architecture is a tool, not a default. It fits well when there are multiple independent reactions to the same fact, spiky or unpredictable load that benefits from buffering, long-running or background work that should not block a user, integration across teams or bounded contexts that must evolve independently, or a need for an audit trail or replayable history.
It is a poor fit when the caller needs an immediate answer (a price quote, an authorization decision), when read-your-writes consistency is required across services, or when the system is small enough that a direct call is simply clearer. A common, sound design is synchronous requests at the edge for queries and user-facing decisions, with events carrying state changes between services behind it.
Topics in This Section
| Page | What it covers |
|---|---|
| Message Brokers & Streaming | Queue vs. log storage; Apache Kafka 4.x (KRaft, share groups, transactions), RabbitMQ 4.x (quorum queues, streams), NATS JetStream, Pulsar; SQS, SNS, EventBridge, Kinesis, and Google Pub/Sub; ordering, delivery semantics, exactly-once, backpressure, and dead-letter queues |
| Event-Driven Patterns | Event sourcing, CQRS, projections, choreographed and orchestrated sagas, the transactional outbox and inbox, idempotent consumers, schema versioning, and eventual consistency |
A suggested reading order is this page for vocabulary, then Message Brokers for the infrastructure and its delivery guarantees, then Event-Driven Patterns for the techniques that make event-driven systems correct.
See Also
- Microservices & Event-Driven: event-driven messaging within a microservice architecture, including durable execution and the outbox pattern
- Async & Event-Driven APIs: webhooks, server-sent events, CloudEvents, and AsyncAPI at the API boundary
- Resilience Patterns: retries, backoff, idempotency, and sagas with compensation
- Testing Distributed Systems: fault injection and consistency checking for asynchronous systems
- Distributed Systems: consistency models, partial failure, and coordination limits
- Database Design: the stores events project into and the outbox table that bridges writes to the broker
Further Reading
- Martin Kleppmann, Designing Data-Intensive Applications: logs, streams, and derived data
- Adam Bellemare, Building Event-Driven Microservices
- Gregor Hohpe and Bobby Woolf, Enterprise Integration Patterns: the canonical messaging-pattern catalog
- Martin Fowler, “What do you mean by ‘Event-Driven’?”
- Jay Kreps, “The Log: What every software engineer should know about real-time data’s unifying abstraction”