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:
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.