A practical lesson on consumers that maintain read models, workflow state, or derived local views, including why replay and correctness become more demanding.
Stateful consumers maintain some form of local derived state across many events. That state may be a projection, a materialized view, a balance snapshot, a search index, or a workflow tracker. This pattern is extremely useful because it lets consumers shape data for specific local needs. It is also more operationally delicate than stateless handling because correctness now depends on how event history is accumulated over time.
The main mindset shift is that the consumer is no longer only reacting to one event in isolation. It is constructing or evolving a local model. That means order, replay, schema changes, and backfills all matter more.
flowchart LR
A["Event stream"] --> B["Stateful consumer"]
B --> C["Projection or read model"]
A --> D["Replay from earlier point"]
D --> B
What to notice:
A projection is a read-optimized or use-specific local view built from events. Common examples include order summary views, customer timelines, shipping dashboards, and search indexes. Projections are valuable because they let consumers query data in the shape they need rather than calling source systems repeatedly.
This often makes stateful consumers one of the strongest downstream patterns in event-driven systems. They improve local autonomy and reduce query chatter across boundaries.
1type OrderProjection = {
2 orderId: string;
3 status: "placed" | "paid" | "shipped";
4 totalAmount: number;
5};
6
7function applyOrderEvent(
8 current: OrderProjection | null,
9 event: { eventName: string; data: any },
10): OrderProjection {
11 if (event.eventName === "order.placed") {
12 return {
13 orderId: event.data.orderId,
14 status: "placed",
15 totalAmount: event.data.totalAmount,
16 };
17 }
18
19 if (!current) {
20 throw new Error("projection missing prior state");
21 }
22
23 if (event.eventName === "payment.settled") {
24 return { ...current, status: "paid" };
25 }
26
27 if (event.eventName === "shipment.dispatched") {
28 return { ...current, status: "shipped" };
29 }
30
31 return current;
32}
The example is intentionally small, but it shows the important difference: the consumer’s correctness depends on how several events combine over time.
Replay matters more for stateful consumers because rebuilding the local model is often part of recovery and evolution. If the consumer logic changes or the local store is lost, the platform may need to replay event history to rebuild the projection. That is useful, but it means event ordering assumptions and schema evolution become much more important than they are for one-off stateless actions.
Stateful consumers do not always need total global order. They often need the correct order within one key or entity lifecycle. But whatever ordering they do rely on must be explicit.
Not all stateful consumers build read models. Some track workflow progression, saga state, or business checkpoints. These consumers are even more sensitive to replay rules and duplicate handling because they may drive later business actions based on accumulated state.
This is why teams should document whether a stateful consumer is:
Those categories have different operational consequences.
A stateful consumer both maintains an account summary projection and triggers fraud review decisions during replay. What should you worry about?
You should worry that replay-safe read-model rebuilding and live action-triggering are mixed together. A consumer that both reconstructs state and emits consequential decisions needs very careful controls so replay does not re-trigger business actions unintentionally.