Delivery guarantees
Not climbed yetModel answer
At-most-once: each message is delivered once or not at all. The consumer acknowledges before processing, so a crash loses the message. Fine for metrics you can afford to drop.
At-least-once: the consumer acknowledges after processing, so a crash before the ack means the message is delivered again. Nothing is lost, but duplicates happen. The usual default.
Exactly-once: each message has its effect exactly once. Across real networks it is achieved as at-least-once delivery plus idempotent or transactional processing, not as a delivery promise.
Model answer
On restart the consumer resumes from the last committed offset, so it processes the message again. With at-least-once delivery this will happen, so the consumer must be idempotent.
- Record each processed message id in a table with a unique constraint, in the same database transaction as the result. A duplicate fails the insert and is skipped.
- Or make the write naturally idempotent: an upsert keyed by the event id, or "set status to shipped" rather than "add one".
- Or store the consumed offset in the same transaction as the result, and seek to it on startup.
Model answer
This is the dual write problem. Database commit then publish fails: the order exists but nobody downstream hears about it (no email, no stock reserved). Publish then commit fails: downstream acts on an order that does not exist. No ordering of the two calls is safe.
Use the transactional outbox: in the same transaction as the order, insert the event into an
outboxtable. A separate relay reads the outbox and publishes to Kafka, marking rows sent. Either both the order and its event are saved or neither is.The relay can poll the table, or tail the database log with CDC (Debezium). The relay may publish an event twice after a crash, so consumers stay idempotent.
Model answer
Kafka keeps order within a partition only. Publish with the order id as the message key: every event for an order hashes to the same partition, and each partition is read by one consumer in the group at a time. Different orders still run in parallel across partitions.
Watch the things that break it:
- Processing messages from one partition concurrently inside the consumer (a thread pool per message) reorders them; parallelise by key, not by message.
- Sending a failed message to a retry topic lets later events for the same order overtake it. Park the whole key until the failed one succeeds, or make handlers tolerate out-of-order events with a version check.
- Adding partitions changes which partition a key maps to for new messages.
