Markdown Editor

Event Delivery Guarantees Template (Outbox, Idempotency, Sagas)

Flow register, delivery semantics, transactional outbox, idempotent consumers, retries and dead letters, exactly-once limits, sagas

SystemdesignStandardTransactional outbox; Sagas (Garcia-Molina and Salem 1987); Apache Kafka 4.4 delivery semanticseventdeliveryguarantees

Anleitung: Use it when services exchange events or messages that must not be lost or applied twice. Fill the flow register with the impact of a lost and a duplicated message first. The main pitfall is trusting an exactly-once claim beyond the boundary that commits output and input position together.

Vorschau

Event Delivery Guarantees: [Flow or service]

Building-block decision record for how messages and events travel between services without being lost or applied twice. It covers the delivery semantics per flow (at-most-once, at-least-once, effectively-once), publishing atomically with a database change (transactional outbox), idempotent consumers, ordering, retries and dead-letter handling, multi-step business transactions as sagas, and the limits of "exactly-once" claims. Concepts draw on the transactional outbox pattern as described on microservices.io, on sagas (Garcia-Molina and Salem, SIGMOD 1987), on Helland's "Life Beyond Distributed Transactions" (ACM Queue, 2016) and on the delivery semantics section of the Apache Kafka 4.4 documentation. Replace every [bracketed] value and every row marked Example. Worked examples use Quillmere, an invented book-review and reading-list platform shared by the other building-block templates in this category.

Document control

FieldValue
Document ID[EVT-REC-001]
Flows covered[Example: Review posted, rating changed, reading goal completed, gift purchase saga]
Broker or transport[Example: Partitioned log broker, topics per event type]
Version[0.1]
Status[Draft / In review / Approved / Implemented / Superseded]
Owner[@platform-events-lead]
Producer teams[@reviews-lead], [@orders-lead]
Consumer teams[@notifications-lead], [@recs-lead]
Last updatedYYYY-MM-DD
Classification[Internal]
RelatedEvent Sourcing and CQRS Design (architecture/event-sourcing-cqrs), Message Queue System Design (system-design/design-message-queue), CDC Pipeline System Design (system-design/design-cdc-pipeline)

Approvals

RoleNameDecisionDate
Platform events[@name][Approved / Changes requested]YYYY-MM-DD
Producer team[@name][Decision]YYYY-MM-DD
Consumer team[@name][Decision]YYYY-MM-DD

Revision history

VersionDateAuthorChange summary
0.1YYYY-MM-DD[@name]First draft
0.2YYYY-MM-DD[@name]Example: Added outbox for review events after lost-notification incident

1. Flow register

IDEventProducerConsumerImpact if lostImpact if duplicatedSemantics chosen
EV-01Example: review.postedReviewsNotificationsAuthor's followers not toldTwo notificationsAt-least-once, dedup by event ID
EV-02Example: rating.changedReviewsRecommendationsSlightly stale model inputCounted twice in a featureAt-least-once, idempotent upsert
EV-03Example: page.viewedWebAnalyticsOne view uncountedOne view overcountedAt-most-once acceptable
EV-04Example: gift.payment.capturedPaymentsOrdersCustomer charged, no gift sentGift sent twiceAt-least-once, idempotent by payment ID, saga (section 7)
[EV-NN][Event][Service][Service][Impact][Impact][Semantics]

2. Delivery semantics

SemanticsMeaningHow it arisesUse when
At-most-onceEach message delivered zero or one timeAcknowledge before processing, no retriesLoss is cheap (telemetry, some analytics)
At-least-onceEach message delivered one or more timesAcknowledge after processing, retry on doubtDefault for business events
Effectively-once (exactly-once processing)The effect of each message is applied once, even if delivered more than onceAt-least-once delivery plus an idempotent consumer, or a transactional read-process-write inside one systemMoney, inventory, anything counted

The Apache Kafka documentation (4.4) explains why: when a producer gets no reply it cannot know whether the write was committed, so resending gives at-least-once behaviour. The idempotent producer removes duplicates caused by producer retries within the log; transactions let a read-process-write application commit its output and its input offsets together, and consumers in read_committed isolation see only committed data. The same documentation notes that writing to an external system needs that system's cooperation to coordinate output with the consumer position.

3. Publishing: transactional outbox

DecisionOptionsExample choice
Relay typePolling publisher, or transaction log tailing (change data capture)Log tailing via the CDC pipeline (system-design/design-cdc-pipeline)
Event IDUUID or database sequence, assigned in the transactionUUIDv7 assigned by the service
OrderingPer aggregate: partition key = aggregate IDPartition key book_id
Outbox cleanupDelete or archive published rows after a retention periodDelete after 3 days
VolumeAverage and peak events per second20,000,000 per day, about 231 per second on average

4. Idempotent consumers

TechniqueHow it worksFits
Natural idempotencyThe operation gives the same result when repeated (set status, upsert by key)State updates
Dedup tableRecord processed event IDs in the same transaction as the effect; skip known IDsAdditive effects (send, count, charge)
Version checkApply only if the event's version is newer than the stored oneOut-of-order updates to one entity
Idempotency key at the targetPass a key to the downstream API so it rejects repeatsPayments and external APIs

Dedup table sizing, Example: 20,000,000 events per day kept for 7 days is 140,000,000 rows; at about 64 bytes per row including index overhead that is 8.96 GB. The retention must exceed the longest redelivery window (broker retention plus replay periods), or a late duplicate is applied again.

5. Ordering, retries and dead letters

RuleExample decision
Ordering scopePer book_id (one partition per key)
Retry policyExponential backoff 1, 2, 4, 8, 16, 32 s then 60 s cap; first attempt plus 8 retries, 183 s of waiting in total
Poison messageAfter the last retry, move to a dead-letter topic with error, attempt count and original offset
Ordering versus retryRetrying event moves to a retry topic; later events for the same key wait behind it (key is parked)
Dead-letter handlingAlert on any message; owning team triages within 1 business day; replay tool re-injects after fix
Schema evolutionConsumers ignore unknown fields; breaking changes use a new event type or version

Backoff schedule, Example:

Retry12345678
Wait before retry (s)124816326060
Cumulative wait (s)137153163123183

Add random jitter to each wait in production so that many consumers do not retry in step; see the Resilience and Load Shedding Design template (system-design/resilience-load-shedding-design).

6. Exactly-once: limits

BoundaryEffectively once?Why
Broker topic to broker topic with transactional read-process-writeYes, within the brokerOutput and input offsets committed together
Broker to consumer database, offset stored in the same database transactionYesEffect and position committed atomically
Broker to consumer database, offset stored in the brokerNo, at-least-onceCrash between database commit and offset commit replays
Consumer sends email or push notificationNo, at-least-onceExternal system cannot join the transaction; dedup by event ID before sending narrows but does not close the gap
Consumer calls a payment API with an idempotency keyYes, if the API honours the key for the retry windowTarget rejects repeats

7. Sagas for multi-service transactions

Example: gift purchase saga (orchestrated).

StepServiceActionCompensationCan it be undone?
1OrdersCreate gift order (pending)Cancel orderYes
2PaymentsAuthorise cardVoid authorisationYes
3InventoryReserve e-book licenceRelease licenceYes
4PaymentsCapture paymentRefundYes, with fees
5NotificationsEmail gift code to recipientNone (cannot unsend)No, so it runs last

For long-running orchestration with timers and durable state, see the Workflow Orchestration Engine System Design template (system-design/design-workflow-orchestration-engine).

8. Monitoring

SignalExample alert
Outbox backlog (unpublished rows, oldest age)Oldest unpublished older than 60 s
Consumer lag per group and partitionLag above 5 minutes
Dead-letter countAny new message
Duplicate rate (dedup table hits per second)Above 1% of deliveries for 15 minutes
End-to-end reconciliationDaily count of review.posted events vs new reviews differs by more than 0
Saga stuck in a non-final stateOlder than 30 minutes

9. Decisions and open items

IDDecision or open itemReasonOwnerStatus
ED-01Example: All business events via transactional outboxNo lost or phantom events on crash@platform-events-leadDecided
ED-02Example: Consumers keep a 7-day dedup table for additive effectsBroker retention is 3 days, replays within 7@notifications-leadDecided
ED-03Example: Gift purchase as orchestrated saga, email lastEmail cannot be compensated@orders-leadDecided
ED-04Example: Decide whether analytics may drop events under loadAt-most-once acceptable for page views@analytics-leadOpen
[ED-NN][Item][Reason][@owner][Status]

Glossary and references

TermMeaning
OutboxTable written in the same transaction as the business change, holding events to publish
RelayProcess that reads the outbox and publishes to the broker
IdempotentApplying an operation more than once has the same effect as once
Dead-letter topicDestination for messages that failed all retries
SagaSequence of local transactions with compensating actions instead of one distributed transaction
CompensationAction that semantically undoes an earlier step

References:

Standard and licence note

Based on the publicly available papers, the Apache Kafka documentation and the pattern description cited above; no standard governs event delivery between services. The text, tables, numbers and examples are original to this collection, and Quillmere is an invented organisation. Broker features differ and change between versions: check the current documentation of the broker and client libraries you use. Template licence: MIT.

Ähnliche Vorlagen