EDA patterns: choreography, orchestration, and event types 💃🕺
EDA is not “one pattern.” It is a family of patterns. And like all families, it can be adorable or dramatic depending on how much you govern it.
Choreography vs orchestration (what you are really choosing) 🎭
Choreography 🧩
flowchart LR
A[Order Service] -->|OrderCreated| EB[(Event broker)]
EB --> P[Payment Service]
EB --> I[Inventory Service]
P -->|PaymentAuthorized| EB
I -->|InventoryReserved| EB
Each service reacts to the events it cares about and publishes new events.
- Advantages: decentralized, scalable, you can add consumers without touching the producer.
- Disadvantages: the business flow can become hard to “see” (and debug) without tools.
When it works well:
- independent side effects;
- simple cascading workflows;
- teams mature in observability.
Orchestration 🧭
flowchart LR
O[Orchestrator / Workflow] -->|commands step| S1[Service A]
O -->|commands step| S2[Service B]
S1 -->|result event| EB2[(Event broker)]
S2 -->|result event| EB2
EB2 --> O
A component (workflow/orchestrator) directs the steps and decides the next action.
- Advantages: visibility into the process, control, central management of errors.
- Disadvantages: you risk a new monolith, only more elegant, with more coupling to the workflow.
When it works well:
- multi-step processes with strong constraints;
- the need to control timeouts, retries, and compensations coherently.
Event Notification 📨
The most minimal payload variant: the event announces that something happened, but carries only a small amount of data—usually just an ID or a reference to the subject involved. The consumer that receives the notification, if it needs details, makes a direct call to the producer to retrieve updated state.
sequenceDiagram
Producer->>Broker: OrderCreated { orderId: 42 }
Broker->>Consumer: OrderCreated { orderId: 42 }
Consumer->>Producer: GET /orders/42
Producer-->>Consumer: { full state }
- Advantages: small and stable contracts, internal state changes do not require renegotiating the event contract, and there is less risk of exposing sensitive data on the async channel.
- Disadvantages: it reintroduces synchronous coupling in disguised form—the consumer must call the producer after receiving the event. If the producer is unreachable at that moment, processing fails or requires separate retry logic. At high volume, it can generate a thundering herd of simultaneous callbacks.
When it works well:
- sensitive data that you do not want to replicate in the broker (GDPR, confidential information);
- state that changes often but with only a few consumers truly interested in the details;
- you want to keep the event contract minimal and decoupled from the internal implementation.
The full discussion—selection criteria, comparison with ECST, and trade-offs around synchronous coupling—is in the event design guide.
Event-Carried State Transfer (ECST) 📦
The alternative to notification: put almost all the relevant state in the same event so the consumer already receives what it needs to process the event without making any calls.
sequenceDiagram
Producer->>Broker: OrderCreated { orderId: 42, userId: 7, items: [...], total: 89.90, ... }
Broker->>Consumer: OrderCreated { orderId: 42, userId: 7, items: [...], total: 89.90, ... }
Note over Consumer: process without callbacks
- Advantages: the consumer is autonomous at runtime—it can process the event even if the producer is temporarily unreachable, zero extra roundtrips, more predictable latency. This is the natural pattern for denormalized read models (typical in CQRS) or high-throughput scenarios where every extra roundtrip is expensive.
- Disadvantages: larger payloads (impact on bandwidth, broker storage, network costs), risk of exposing sensitive data in the stream, and—more subtly—schema evolution becomes more complex: every field in the payload becomes part of the public contract, and removing or renaming something can break consumers you did not even expect.
When it works well:
- consumers that must be highly autonomous or operate in an offline-first mode;
- high-throughput scenarios where callback cost is significant;
- building specialized read models (projections/materialized views in CQRS style).
For the full treatment with selection criteria, comparison with Event Notification, and schema evolution strategies, see the event design guide.
Event Sourcing and CQRS 🏛️
In mature EDA systems, sooner or later the question arrives: “is reacting to events enough, or do we also need to reconstruct state and optimize reading?” This is where Event Sourcing (the event history as an immutable source of truth) and CQRS (separate write and read models) come in.
They are not mandatory: they are useful when audit, replay, and specialized read models are real requirements, not whiteboard wishes. For a full treatment—snapshots, schema evolution, and adoption criteria—see the dedicated guide on Event Sourcing and CQRS.
What to decide really (without getting lost in theory) 🧰
This guide is not here to collect patterns like trading cards: it is here to help you choose. When you are designing an event-driven flow, the decisions that matter are few and fairly blunt.
- Choreography: if the flow is linear and side effects are independent, it is usually enough. Example:
OrderCreated→ reserve stock → confirm; you avoid a single central point, but you must accept that coordination and timeouts are distributed. - Orchestration: if you need visibility and centralized control over the process (state, timeouts, coherent compensation), it makes sense. Useful for centralized policies (retry, circuit breaker) on multi-step processes, but you gain more coupling and more maintenance overhead.
- Notification: if you want smaller, more stable contracts, it reduces payload size. But it often reintroduces synchronous coupling: consumers must fetch data from the producer and the async model becomes a dependency with variable latency.
- ECST: if you want highly autonomous consumers, it reduces callbacks and roundtrips. In return, you increase responsibilities and risks around payload size, privacy, and schema evolution/versioning.
- Event Sourcing / CQRS: these are not decisions to make on the first whiteboard. They become relevant when you have real requirements for audit, replay, or very specialized read models. If you do not know yet whether you need them, you probably do not need them. When you do, see the dedicated guide.
Practical note: whatever you choose, prepare to measure it. Without observability (correlation IDs, decent tracing/logging), “choreography vs orchestration” becomes a philosophical discussion — and chaos usually wins.
Next steps 🚀
Now that you chose how to design the flows (choreography/orchestration) and what payload style to use, the rest is discipline: contracts and operations.
- To understand queue/stream/pub-sub: see the messaging guide.
- For contracts and schema evolution: see the event design guide.
- For idempotency, retry, and DLQ: see the operational guide.