Skip to content
AB

Building Event-Driven Features Without Losing Consistency

Use ownership, durable events, idempotent consumers, and rebuildable projections to keep asynchronous features understandable.

Ashish BhakhandPublished

Suppose completing a lesson updates progress, extends a streak, awards a milestone, creates an inbox item, and sends a notification. Doing all five steps in one request makes the response slow and couples unrelated features. Sending loose events without rules creates a different problem: duplicates, missing effects, and unclear ownership.

One feature owns each truth#

The progress feature owns lesson completion. The streak feature owns streak state. Notifications own delivery. Other features request or react to changes; they do not write another feature's collections directly.

Use events in past tense to describe facts:

Code
type LessonCompleted = {
  eventId: string;
  type: "lesson.completed";
  occurredAt: string;
  userId: string;
  lessonId: string;
};

An event should contain the stable identifiers and facts consumers need, not an entire mutable database document.

Publish durably#

Writing progress and then publishing an event creates a failure gap. The database may commit while publishing fails. An outbox closes that gap by storing the state change and an event record in the same durable operation or transaction. A worker later publishes pending outbox records.

The publisher can retry because consumers are idempotent.

Make every consumer replay-safe#

Each consumer records eventId or uses a natural business key with a unique constraint. Processing the same lesson.completed event twice must not award the milestone twice.

Consumers should also tolerate relevant ordering problems. If strict order is essential, choose a partition key and ordered transport deliberately. Otherwise, design updates around versions, timestamps, or commutative operations.

Accept and expose eventual consistency#

The API may confirm lesson completion before the new badge appears. That is not automatically a bug; it is a consistency decision. The product should communicate pending work where the delay is visible and provide a recovery path when asynchronous processing fails.

Derived views should be rebuildable from source facts when practical. Rebuildability turns a projection bug from permanent corruption into a repair operation.

Event-driven architecture helps when features need independent ownership, scaling, and failure handling. It is not a shortcut around consistency. Durable publication, stable event contracts, idempotent consumers, and explicit delay are what make it reliable.

← Back to all writing