Unit 5 · Distributed & cloud · Lesson 6 of 14
Delivery semantics, deduplication and the outbox pattern
Step 1: Learn the ideas
- Message queues versus partitioned logs
- At-most-once, at-least-once and effectively-once
- Sources of duplicates and reordering
- Idempotency keys and deduplication windows
- Naturally idempotent operations and conditional writes
- Dual writes and the transactional outbox
- Change data capture as an outbox relay
- Consumer offsets and commit ordering
- Idempotent producers and transactions in log brokers
- Per-key ordering, poison messages and dead-letter queues
Why it matters
Asynchronous messaging is how systems absorb load spikes and decouple teams, and it is also where duplicate charges, double emails and lost orders come from. "We use a queue, so it is reliable" invites the follow-up: what happens when the consumer crashes after doing the work but before acknowledging it? The answer always combines delivery semantics with idempotency, so you need both.
Core ideas
Delivery semantics follow from acknowledgment order.
- At-most-once: acknowledge (or commit the offset) before processing. A crash mid-processing loses the message, but nothing is processed twice.
- At-least-once: process, then acknowledge. A crash after processing but before the acknowledgment causes redelivery, so duplicates are normal.
- Exactly-once delivery over an unreliable network is not achievable as a transport property. What systems offer is effectively-once processing: at-least-once delivery plus deduplication, or an atomic commit of results and progress, within a defined boundary.
Where duplicates and reordering come from. Producers retry after timeouts with unknown outcomes; brokers redeliver after visibility timeouts or consumer-group rebalances; consumers crash between side effect and acknowledgment. Order holds at most within one partition or message group, and retries or parallel consumers can still reorder unless processing is serialized per key.
Idempotent consumers. Make processing safe to repeat:
- Prefer naturally idempotent writes (set status to SHIPPED, upsert by primary key) over increments.
- Give every message or request a unique ID (an idempotency key chosen by the producer), record processed IDs in the same transaction as the effect, and skip IDs already recorded. Retain IDs at least as long as redelivery is possible.
- For external side effects (an email, a charge), pass the key to the provider if it supports one, or record intent and outcome so a retry can check before acting.
The dual-write problem. A service that updates its database and then publishes an event can crash between the two, or have one succeed and the other fail, leaving them inconsistent. Distributed transactions spanning a database and a broker are rarely available or worth it. The transactional outbox fixes this: in the same local transaction as the state change, insert the event into an outbox table; a relay (a poller, or change data capture on the database log) publishes outbox rows and marks them sent. The relay may publish a row twice, so consumers still deduplicate.
BEGIN
UPDATE orders SET status = 'PAID' WHERE id = 42
INSERT INTO outbox(event_id, topic, payload) VALUES ('e-9f1', 'order-paid', ...)
COMMIT
relay: read unsent outbox rows -> publish -> mark sent (at-least-once)
consumer: if event_id already processed: skip
else: apply effect and record event_id in one transaction
Consumer offsets. In a partitioned log, each consumer group tracks a position per partition. Committing before processing gives at-most-once; committing after gives at-least-once. Storing the offset in the same database transaction as the results makes the pair atomic, which is effectively-once for that sink.
What "exactly-once" means in a log broker. Idempotent producers attach sequence numbers so the broker drops retried duplicates within a producer session. Transactions let a read-process-write job atomically write its output records together with its input offsets, and consumers configured to read only committed data never see aborted output. The boundary ends at the broker: a database row written or an API called by the consumer is outside it and needs its own idempotency. Managed queues make narrower promises still; one deduplicates only within a fixed time window.
Common misconceptions
- "Our broker guarantees exactly-once, so consumers need not be idempotent." The guarantee covers a specific pipeline inside the broker; anything touching an external system can repeat.
- "Acknowledging after processing avoids duplicates." It avoids loss and causes duplicates on crash; idempotency removes their effect.
- "Publishing right after the commit is good enough." The process can die between the two; use an outbox or change data capture.
- "A queue preserves order." Usually only per partition or message group, and only with one in-flight message per key.
- "Deduplication keys can be kept forever." Retention must cover the redelivery horizon; choose it explicitly and expire the rest.
How to approach it
Read DDIA Ch. 11 (messaging systems, partitioned logs and fault tolerance), then the Kafka design notes on delivery semantics and the outbox pattern description. Write consumer pseudocode for both offset orders and mark the crash points that cause loss or duplicates. Graduates should know the three semantics and why idempotency matters. Mid-level candidates should design an outbox and idempotent consumers. Senior candidates should define the exactly-once boundary, per-key ordering and the operational handling of poison messages and backlogs.
Suggested order
- 1Read DDIA Ch. 11 (Stream Processing): "Transmitting Event Streams" for queues versus logs, and the "Fault Tolerance" section for idempotence and atomic commit.
- 2Read the Kafka design notes on consumer position, message delivery semantics and using transactions; write down exactly what the exactly-once boundary covers.
- 3Read the AWS transactional outbox page and the Builders' Library article on idempotent APIs; design the outbox and processed-ID tables for an order service.
- 4Read the SQS pages on visibility timeout and FIFO exactly-once processing to see how a managed queue scopes its guarantees.
- 5Write consumer pseudocode for commit-before-processing and commit-after-processing and mark the crash points that cause loss or duplicates.
- 6Do the Gossip Glomers Kafka-style log challenge or 6.5840 Lab 2 to implement offsets or duplicate suppression yourself.
Step 2: Check off what you can do
Tap each one you can genuinely do or explain. Each earns 10 XP.
Step 3: Go deeper
Apache Kafka documentation: Design (4.3)Start here
Apache Software FoundationDelivery semantics, consumer positions, idempotent producers and transactions as documented by a widely used log broker.
Open(opens in a new tab)Link responded, 5 Oct 2026Designing Data-Intensive ApplicationsStart here
O'Reilly Media (author's book site)Section: Ch. 11: Stream Processing
Queues versus partitioned logs, idempotence and fault tolerance in stream processing.
Open(opens in a new tab)Link responded, 5 Oct 2026Transactional outbox pattern (AWS Prescriptive Guidance)
Amazon Web ServicesThe dual-write problem and the outbox fix.
Open(opens in a new tab)Link responded, 5 Oct 2026Making retries safe with idempotent APIs
Amazon Builders' LibraryDesigning idempotency keys: atomic recording with the effect, equivalent responses to duplicates and rejecting mismatched reuse.
Open(opens in a new tab)Link responded, 5 Oct 2026Amazon SQS Developer Guide: at-least-once delivery, visibility timeout and exactly-once processing
Amazon Web ServicesA managed queue's at-least-once delivery, visibility timeout and time-bounded deduplication.
Open(opens in a new tab)Link responded, 5 Oct 2026
Step 4: Practice
Implement the Kafka-style log challenge in Gossip Glomers (offsets and committed positions) or the 6.5840 key/value lab (duplicate suppression for retried writes). As a design drill, take an order service that charges a card and sends a confirmation email: draw the outbox, relay, consumers and deduplication store, then walk through a crash at every arrow and confirm that no charge or email can happen twice or be lost.
- Gossip Glomers distributed-systems challenges (Maelstrom)Open(opens in a new tab)Not attemptedFly.io with Jepsen Maelstrom
- MIT 6.5840 Lab 2: Key/value server with at-most-once Puts and a lockOpen(opens in a new tab)Not attemptedMIT 6.5840
Record attempts on each problem’s page (+15 XP, +10 more when solved on your own).
Step 5: Test yourself
Answer each one out loud or on paper before you finish the lesson.
- Question 1A consumer charges a card and crashes before committing its offset. What happens next, and how do you make it safe?
- Question 2Why is "write to the database, then publish" unsafe even when both calls usually succeed?
- Question 3Why must the processed-ID record be written in the same transaction as the effect?
- Question 4How can per-customer ordering be preserved with parallel consumers?
- Question 5What is a poison message and how should a pipeline handle it?
- Question 6What does an idempotent producer protect against, and what does it not?
You’re done when you can
- Explain at-most-once, at-least-once and effectively-once with the exact crash point that distinguishes them.
- Design an outbox-based publisher and an idempotent consumer for an unseen scenario in under 20 minutes.
- State the scope of an exactly-once claim for a log broker and name one effect it does not cover.
- Choose and justify a retention period for deduplication keys.
Finish the lesson
Unlocks next: Stream processing: event time, watermarks and replay


