"Event-driven" gets used as a catch-all for anything involving a message queue. That's too broad to be useful when you're actually designing a system. The patterns underneath the label make very different trade-offs, and picking the wrong one is expensive to unwind later.
- Event notification (thin events) and event-carried state transfer (fat events) solve different problems — know which one you're building before you design the schema.
- Choreography scales better with fewer services; orchestration is easier to reason about once you have more than a handful of steps.
- Eventual consistency is a real design constraint, not a footnote — it changes what your UI and your support team need to handle.
- Not every system benefits from EDA. Simple CRUD with strong consistency needs is often better served by direct calls.
Request/response vs event-driven
In a request/response system, a service calls another service and waits for the answer. The caller knows what it needs and who has it. In an event-driven system, a service publishes a fact about something that happened — OrderPlaced, PaymentCaptured — without knowing or caring who's listening. That inversion is the whole point: it decouples producers from consumers, lets you add new consumers without touching the producer, and lets services scale and fail independently.
It also means you give up the guarantee that a chain of actions completes as a single atomic unit. That trade-off is the thing to get right before writing any code.
Event notification vs event-carried state transfer
These are the two shapes an event can take, and they lead to very different consumer designs.
// Event notification (thin) -- consumers call back for detail
{
"eventType": "OrderPlaced",
"orderId": "8f14e45f-...",
"occurredAt": "2026-07-06T14:02:00Z"
}
// Event-carried state transfer (fat) -- consumers have what they need
{
"eventType": "OrderPlaced",
"orderId": "8f14e45f-...",
"customerId": "c-9928",
"lines": [{ "sku": "WIDGET-1", "quantity": 3, "unitPrice": 19.99 }],
"total": 59.97,
"occurredAt": "2026-07-06T14:02:00Z"
}
Thin events keep the producer's schema small and avoid leaking internal state, but every consumer now makes a synchronous call back to the source service to get details — reintroducing the coupling you were trying to remove. Fat events avoid the callback but mean every consumer receives (and must version against) a copy of data it might not need. In practice, most systems land on a middle ground: include what consumers commonly need, and offer a lookup API for the rest.
Choreography vs orchestration
Say placing an order needs to reserve inventory, charge a card, and schedule shipping. Two ways to coordinate that:
Choreography: each service reacts to the previous service's event and emits its own. Inventory listens for OrderPlaced and emits StockReserved; Payments listens for that and emits PaymentCaptured; Shipping listens for that and emits ShipmentScheduled. No service knows about the whole flow — it emerges from individually simple reactions.
Orchestration: a saga orchestrator explicitly calls each step, tracks progress, and issues compensating actions if something fails.
public class PlaceOrderSaga
{
public async Task ExecuteAsync(PlaceOrderCommand command, CancellationToken ct)
{
var reservation = await _inventory.ReserveAsync(command.Lines, ct);
try
{
var payment = await _payments.ChargeAsync(command.CustomerId, command.Total, ct);
await _shipping.ScheduleAsync(command.OrderId, ct);
}
catch (Exception)
{
await _inventory.ReleaseAsync(reservation.Id, ct); // compensating action
throw;
}
}
}
Choreography scales well with two or three participants but gets hard to debug past that — there's no single place to see "what happens when an order is placed." Orchestration adds an explicit coordinator, which is more code up front but keeps the flow, the failure handling, and the compensation logic in one readable place. For anything with more than three or four steps, or any step that needs a compensating action on failure, we default to orchestration.
When not to use event-driven architecture
Skip it when the operation genuinely needs strong consistency (a bank transfer between two accounts you own is usually better as a single transaction), when the system is small enough that the operational overhead of a message broker outweighs the decoupling benefit, or when your team doesn't yet have the operational maturity to debug distributed failures — dead letters, duplicate delivery, and out-of-order events are a permanent tax, not a one-time cost.