A practical lesson on what consumers do with events, how handler design affects coupling and operations, and why consumer responsibility matters as much as producer design.
Consumers are the systems that react to events. That reaction may be tiny, such as sending a notification, or substantial, such as building a projection, coordinating a workflow, enriching data, or feeding analytics. The important architectural point is that a consumer should own a clear downstream responsibility. If consumers exist only to immediately ask the producer what to do next, the system has moved coupling into a slower and harder-to-debug channel rather than actually improving autonomy.
Consumer design matters because EDA moves real responsibility into the downstream edge. A weak consumer can turn retries into duplicate side effects, hide business logic across too many handlers, or make operations impossible because nobody can explain which consumer owns which outcome.
sequenceDiagram
participant B as Broker
participant C as Consumer
participant S as Local Store
B-->>C: Deliver event
C->>C: Validate and interpret
C->>S: Apply local effect
C-->>B: Acknowledge success
What to notice:
Consumers typically fall into a few broad categories:
These categories can overlap, but they should not blur too freely. One consumer trying to do all of them often becomes a hidden coordinator or an operational bottleneck.
The strongest consumers can interpret events through stable contracts and act within their own boundary. That does not mean they need no dependencies. It means the consumer’s purpose remains clear. A fulfillment consumer should know how to convert order.placed into local fulfillment work. An analytics consumer should know how to derive metrics. Neither should depend on guessing what another downstream consumer might do.
This is why event-driven design is not just “publish now, figure it out later.” Consumer responsibility must be as deliberate as producer responsibility.
Some consumers are nearly stateless. They read an event, perform a bounded action, and acknowledge it. Others are stateful and maintain local read models, workflow state, or materialized views. Both are valid. The key difference is operational burden. Stateful consumers need stronger replay discipline, ordering assumptions, and recovery planning because their local correctness accumulates over time.
1type EventEnvelope = {
2 eventId: string;
3 eventName: string;
4 occurredAt: string;
5 data: Record<string, unknown>;
6};
7
8async function handleOrderPlaced(event: EventEnvelope) {
9 if (event.eventName !== "order.placed") return;
10
11 await projectionStore.upsertOrderSummary({
12 orderId: String(event.data.orderId),
13 customerId: String(event.data.customerId),
14 totalAmount: Number(event.data.totalAmount),
15 status: "placed",
16 });
17}
The code is intentionally small, but it illustrates a healthy consumer idea: interpret one event clearly, apply one local responsibility, and leave unrelated concerns elsewhere.
One consumer smell appears when handlers immediately call back to the producer for every important decision or field. Sometimes that is necessary, especially with lightweight notification patterns. But if most consumers do this routinely, the architecture is not as loosely coupled as it appears. The system still depends on the producer’s live availability and internal model.
Another smell appears when consumers silently encode workflow assumptions that nobody documented. A chain of handlers can become a distributed hidden process long before the team notices it.
Consumers should be operationally visible. Teams should know:
That visibility matters because consumer failure is often where business consequences become visible. A producer may publish correctly while one critical consumer quietly stops making progress.
1consumer:
2 name: order-summary-projection
3 handles:
4 - order.placed
5 - payment.settled
6 - shipment.dispatched
7 effect: update read model
8 replaySafe: true
A consumer receives order.placed, then immediately calls the order service, pricing service, and customer service before it can act. What should you challenge?
The main challenge is whether the event contract is too thin or the consumer boundary is poorly shaped. If a consumer cannot do useful local work without several synchronous callbacks, the architecture may still be tightly coupled despite using asynchronous transport.