At-most-once, at-least-once, exactly-once; idempotency key, DLQ, ordering
Pesan bisa hilang atau datang dua kali. Pilih mana yang Anda terima, lalu buat consumer yang aman terhadap duplikat
Sebelumnya, lesson 0016: queue dan pub/sub memisahkan producer dari consumer, dan Kafka menyimpan posisi consumer sebagai offset. Lesson ini menjawab pertanyaan lanjutan yang hampir selalu muncul di interview: "Bagaimana kalau consumer crash di tengah jalan?"
1. Tiga delivery semantics
Kafka docs mendefinisikan ketiganya dengan ringkas [1]:
| Semantik | Definisi (Kafka docs) | Artinya untuk Anda |
|---|---|---|
| at-most-once | "Messages may be lost but are never redelivered." | Boleh hilang: metrik, log debug, notifikasi "sedang mengetik" |
| at-least-once | "Messages are never lost but may be redelivered." | Tidak boleh hilang, tapi consumer harus tahan duplikat |
| exactly-once | "Each message is processed once and only once." | Paling mahal, dan biasanya hanya berlaku di dalam satu sistem |
Semantik mana yang Anda dapat ditentukan oleh urutan dua langkah di consumer: memproses pesan dan menyimpan posisi (offset atau ack).
Kafka docs menjelaskan kedua urutan ini persis seperti di atas, lalu menyimpulkan bahwa Kafka "guarantees at-least-once delivery by default". At-most-once didapat dengan mematikan retry di producer dan commit offset sebelum memproses. Exactly-once tersedia untuk alur baca-proses-tulis di dalam Kafka (Kafka Streams, transactional producer), tetapi "exactly-once delivery for other destination systems generally requires cooperation with such systems." [1]
RabbitMQ sama: dengan manual ack, pesan yang belum di-ack "is automatically requeued" saat koneksi consumer putus, sehingga "consumers must be prepared to handle redeliveries and otherwise be implemented with idempotence in mind." [2] Amazon SQS standard queue juga terus terang: Anda "might get that message copy again", jadi "design your applications to be idempotent." [3]
Kalimat yang aman di interview: "Saya asumsikan at-least-once, lalu membuat efeknya exactly-once dengan consumer yang idempotent." Ini lebih jujur daripada "broker kami exactly-once".
2. Idempotency: duplikat tidak boleh berefek dua kali
Idempotent berarti menjalankan operasi yang sama berkali-kali menghasilkan efek yang sama dengan menjalankannya sekali. Kafka docs memberi contoh termurah: kalau pesan punya primary key, update-nya sudah idempotent, karena "receiving the same message twice just overwrites a record with another copy of itself." [1] Masalahnya ada pada operasi yang menambah sesuatu: tagih kartu, kirim email, kurangi stok.
Idempotency key di API
Untuk request HTTP yang di-retry (timeout, koneksi putus), Stripe memakai idempotency key: client membuat "a unique key that the server uses to recognize subsequent retries of the same request", disarankan V4 UUID, maksimal 255 karakter. Server menyimpan status code dan body hasil pertama, dan "retries with the same key return the saved response, including 500 errors." Server juga membandingkan parameter request; kalau berbeda untuk key yang sama, request ditolak. Key boleh dibuang setelah minimal 24 jam. [4]
POST /v1/payments
Idempotency-Key: 6f1c2a3e-9b7d-4e21-a0c4-5d8e7f9a1b2c
{"order_id": 42, "amount": 150000}
→ 201 Created {"payment_id": "pay_901"} ← request pertama, hasil disimpan per key
(timeout di sisi client, client mengulang dengan key yang sama)
→ 201 Created {"payment_id": "pay_901"} ← hasil tersimpan, kartu tidak ditagih lagi
Consumer idempotent
Di sisi consumer queue, polanya sama: setiap pesan membawa ID unik, dan consumer mencatat ID yang sudah diproses dalam transaksi yang sama dengan efeknya.
// Dibaca, bukan dijalankan. processed_messages.message_id punya UNIQUE constraint.
async function handle(msg: { id: string; orderId: number; amount: number }) {
await db.transaction(async tx => {
const inserted = await tx.query(
"INSERT INTO processed_messages (message_id) VALUES ($1) ON CONFLICT DO NOTHING",
[msg.id]);
if (inserted.rowCount === 0) return; // duplikat: sudah pernah diproses, lewati
await tx.query("INSERT INTO payments (order_id, amount) VALUES ($1, $2)", [msg.orderId, msg.amount]);
}); // ack/commit offset SETELAH transaksi commit
}
Kalau efeknya ada di sistem luar (gateway pembayaran), teruskan ID yang sama sebagai idempotency key ke sistem itu. Itulah "cooperation with such systems" yang dimaksud Kafka docs.
Latihan 1 — Idempotency key dan consumer idempotent
Jawab tanpa scroll ke atas. Soal yang sama akan muncul lagi, diacak, di review 0026.
Kuis di lesson ini adalah feedback cepat, bukan bukti penguasaan; yang dicatat hanya hasil review 0026.
3. Dead letter queue: pesan yang terus gagal
Pesan dengan format rusak (sering disebut poison message) akan gagal setiap kali diproses. Kalau consumer terus me-requeue, RabbitMQ docs memperingatkan terjadinya "a requeue/redelivery loop". [2] Obatnya adalah batas percobaan, lalu pindahkan ke dead letter queue (DLQ):
- Amazon SQS: redrive policy dengan
maxReceiveCount, yaitu "the number of times a consumer can receive a message from a source queue before it is moved to a dead-letter queue." Set cukup tinggi untuk retry yang wajar. DLQ "are useful for debugging your application because you can isolate unconsumed messages." [5] - RabbitMQ: pesan di-dead-letter ke sebuah exchange (DLX) saat di-reject/nack tanpa requeue, kedaluwarsa karena TTL, dibuang karena queue melewati batas panjang, atau dikembalikan ke quorum queue lebih dari
delivery-limit. [6]
DLQ bukan tempat sampah: pasang alarm pada jumlah pesannya, selidiki penyebabnya, perbaiki, lalu kirim ulang (redrive).
4. Ordering: hanya per key, dan ada harganya
Urutan global di sistem terdistribusi itu mahal. Yang realistis adalah ordering per key: di Kafka, event dengan key sama (mis. account_id) masuk ke partition yang sama dan dibaca berurutan (lesson 0016).
Di Amazon SQS, FIFO (first-in-first-out) queue menjaga urutan dan menghindari duplikat saat producer mengulang SendMessage "within the 5-minute deduplication interval".
[7]
Dua jebakan: (1) menambah consumer paralel tanpa key merusak urutan; (2) DLQ juga bisa merusak urutan. SQS docs: "Don't use a dead-letter queue with a FIFO queue if you don't want to break the exact order of messages or operations." [5] Jadi tanyakan dulu ke interviewer: urutan apa yang benar-benar penting, per akun, per pesanan, atau tidak sama sekali?
Latihan 2 — Semantik, DLQ, ordering
Jawab tanpa scroll ke atas. Soal yang sama akan muncul lagi, diacak, di review 0026.
5. Kapan tidak perlu
Sinyal yang membenarkan: exactly-once "sungguhan" (transaksi Kafka, FIFO queue) layak dibayar kalau duplikat berarti uang hilang dan efeknya tidak bisa dibuat idempotent dengan murah. DLQ layak kalau ada pesan yang bisa gagal permanen dan Anda tidak boleh membuangnya diam-diam. Ordering ketat layak kalau urutan mengubah hasil (debit sebelum kredit), bukan hanya "lebih rapi".
Biaya dan risiko: jaminan yang lebih kuat dibayar dengan latency dan throughput (contoh kecil: producer Kafka yang menunggu pesan ter-commit ke semua replica in-sync bisa butuh "on the order of 10 ms" per pengiriman [1]);
ordering per key membatasi paralelisme pada jumlah key aktif; tabel processed_messages tumbuh dan butuh pembersihan; DLQ tanpa alarm hanya menunda masalah.
Alternatif lebih murah:
- Upsert berdasarkan primary key sudah idempotent; tidak butuh tabel dedup.
- At-least-once + consumer idempotent hampir selalu cukup dan lebih murah daripada exactly-once di broker.
- At-most-once untuk data yang boleh hilang (metrik, presence): jangan bayar retry untuk itu.
- Satu transaksi database kalau semua efek ada di satu database: tidak perlu queue, tidak perlu idempotency layer.
Latihan lisan — Ucapkan dengan suara keras
Pasang timer dan jawab dengan suara keras, seolah sedang interview. Jangan buka jawaban contoh sebelum selesai.
"Queue Anda at-least-once. Bagaimana Anda memastikan pembayaran tidak terjadi dua kali, dan apa yang Anda lakukan dengan pesan yang terus gagal?" (60 detik)
Bandingkan dengan jawaban contoh dan checklist-nya
"At-least-once berarti pesan tidak hilang tapi bisa datang dua kali, misalnya kalau consumer crash setelah memproses tapi sebelum ack atau commit offset. Exactly-once end-to-end ke sistem eksternal tidak saya andalkan dari broker, jadi consumer saya buat idempotent. Setiap pesan membawa ID unik; dari sisi API, client mengirim Idempotency-Key, misalnya UUID. Consumer mencatat ID yang sudah diproses di tabel dengan unique constraint, dalam transaksi yang sama dengan efek pembayarannya, jadi duplikat dilewati dan mengembalikan hasil lama. Pesan yang terus gagal saya batasi, misalnya lima percobaan dengan backoff, lalu dipindah ke dead letter queue dan diberi alarm, supaya tidak memblokir pesan lain. Kalau volumenya kecil dan semua dalam satu database, cukup transaksi biasa tanpa queue sama sekali."
Checklist: apakah Anda menyebut…
- At-least-once: pesan bisa duplikat, dengan contoh penyebabnya (crash sebelum ack/commit offset)
- Tidak mengandalkan exactly-once dari broker untuk sistem eksternal
- Idempotency key / ID pesan unik, dicatat bersama efeknya (unique constraint atau transaksi)
- Duplikat mengembalikan hasil tersimpan, bukan menagih lagi
- Batas retry lalu dead letter queue, dengan monitoring/alarm
- Membenarkan komponen dengan angka, atau menyebut kapan cukup transaksi database biasa
Lulus jika minimal 4 dari 6.
Berikutnya, lesson 0018: event-driven, outbox pattern untuk menghindari dual write, dan back-pressure saat consumer tertinggal.
Istilah: GLOSSARY.md (bagian "Asinkron") · Indeks: semua lesson System Design
Ada yang belum jelas? Tanyakan ke saya (agent) di terminal, misalnya: "Kenapa ack setelah commit transaksi, bukan sebelumnya?", "Berapa maxReceiveCount yang masuk akal untuk job yang memanggil API pihak ketiga?"
Sumber: [1] Apache Kafka 4.4 docs: Design (Message Delivery Semantics) · [2] RabbitMQ: Consumer Acknowledgements and Publisher Confirms · [3] Amazon SQS: At-least-once delivery · [4] Stripe API Reference: Idempotent requests · [5] Amazon SQS: Using dead-letter queues · [6] RabbitMQ: Dead Letter Exchanges · [7] Amazon SQS: Exactly-once processing (FIFO)