Skip to main content

Notification Event Idempotency

Summary

Duplicate suppression is a persisted ledger keyed on the event identifier, checked at the start of processing and written at the end. The check is uniquely indexed, so duplicates cannot be stored — but the window between check and write is wide.

Audience

Engineers, architects, QA, support, DevOps engineers and security reviewers.

Idempotency lifecycle

The dashed box is the exposure window: everything inside it happens before the ledger row exists.

The ledger

One row per consumed event in the processed-events table, carrying the event identifier, type, tenant, correlation, source service, the outcome and the count of messages created. The event identifier is uniquely indexed.

Seven outcomes are recorded: processed, duplicate, stale, no template, no recipient, suppressed and failed.

The duplicate check

A no-tracking existence query on the event identifier, executed as the first step of dispatch. On a hit the dispatcher writes an audit row at the duplicate stage and returns a duplicate result without writing a second ledger row, so the ledger keeps exactly one row per event while the audit trail records every redelivery seen.

The consumer acknowledges that result normally, so a duplicate is consumed and discarded rather than requeued.

The staleness guard

A second guard compares the envelope's occurrence timestamp against a configurable window, defaulting to 168 hours. A zero value disables the check.

A stale event is audited, recorded in the ledger with a stale outcome, and skipped. Recording it matters: it means a stale event is not reprocessed if redelivered later.

Failure windows

The check-then-write window. The existence check runs first and the ledger row is written last, with the entire pipeline — including provider sends and bounded retry delays — in between. Two concurrent deliveries of the same event can both pass the check and both process fully, duplicating messages and deliveries. The unique index rejects the second ledger insert, but only after the duplicate side effects have occurred, and that violation is not caught, so it escapes as an unhandled exception and the consumer requeues.

The crash window. A crash or a shutdown cancellation mid-processing leaves committed messages and audit rows with no ledger row, because the ledger is written last. Redelivery finds no ledger row and reprocesses from the beginning, duplicating the work already committed.

The missing-identifier window. When a message arrives with neither a message id nor an event-id header, the consumer generates a fresh identifier per delivery. Such a message is never recognisable as a duplicate, so every redelivery is treated as new.

What idempotency does not cover

The ledger deduplicates events, not notifications. The manual retry endpoint, the test-dispatch hook with a caller-supplied identifier, and the template test send all create messages outside the ledger's protection.

Growth

The ledger is insert-only and never trimmed, so suppression never expires — and it cannot be trimmed safely, because an absent row is indistinguishable from an unprocessed event.

Classification

Foundation — the mechanism is Implemented and structurally sound; the windows around it are unmitigated.

Requires confirmation

Whether an early ledger claim, unique-violation handling, or a bounded suppression horizon are planned requires confirmation.

See Also

Keywords

  • Notification database
  • Idempotency
  • Draft database documentation

Source References

  • microservices/src/notification-service/Application/NotificationDispatcher.cs
  • microservices/src/notification-service/Domain/NotificationEntities.cs
  • microservices/src/notification-service/Application/NotificationOptions.cs
  • microservices/src/notification-service/Messaging/NotificationEventConsumer.cs
  • microservices/src/notification-service/Infrastructure/NotificationDbContext.cs

Revision Information

  • Status: Draft
  • Last reviewed: 2026-07-21
  • Review cycle: Quarterly