Idempotency Key Membatasi Mutasi Duplikat Saat Retry
Client dapat kehilangan respons dari mutasi yang sebenarnya berhasil. Server mungkin sudah melakukan commit untuk pembayaran, reservasi, atau submission job, lalu koneksi terputus sebelum respons mencapai caller. Dari sisi client, timeout tidak menunjukkan apakah mutasi gagal sebelum commit atau berhasil sebelum respons hilang.
Retry diperlukan untuk menjaga availability, tetapi retry tanpa proteksi dapat mengulang side effect. Idempotency key memberi identitas stabil pada kedua attempt sehingga server dapat memperlakukannya sebagai satu operasi logis.
attempt 1: POST /orders key=8b7a... -> commit -> respons hilang
attempt 2: POST /orders key=8b7a... -> kembalikan outcome tersimpanKey tidak otomatis membuat handler apa pun menjadi idempotent. Correctness berasal dari protocol penyimpanan yang mengklaim key, mengikatnya ke request, mencatat state operasi, dan mencegah attempt pesaing menjalankan mutasi yang sama secara independen.
Key menamai satu operasi logis
Key baru berlaku untuk satu mutasi yang dimaksud, bukan untuk satu transport attempt. Retry memakai key yang sama. Mutasi terpisah mendapat key berbeda meski payload-nya kebetulan identik.
Pemisahan ini mencegah deduplication yang keliru terhadap tindakan berulang yang memang sah. Dua pembelian barang yang sama dapat memiliki request body identik tetapi tetap mewakili dua order yang berbeda.
Key umumnya dibuat client dari ruang acak yang besar seperti UUIDv4. Server sebaiknya memberi scope secara eksplisit, misalnya berdasarkan tenant atau account, agar operasi milik satu caller tidak bertabrakan dengan namespace caller lain.
dedupe identity = (account_id, idempotency_key)Server juga memerlukan retention policy. Key tidak dapat menahan duplikat setelah record-nya kedaluwarsa. Dokumentasi API karena itu perlu menyatakan periode replay yang didukung untuk key yang sama.
Klaim key dan mutasi state memerlukan satu correctness boundary
Pemeriksaan yang diikuti insert terpisah rentan terhadap race.
request A: lookup key -> tidak ada
request B: lookup key -> tidak ada
request A: jalankan mutasi
request B: jalankan mutasiKedua request melihat state kosong yang sama. Sistem memerlukan klaim atomik, biasanya melalui unique constraint, conditional write, operasi compare-and-set, atau transaction yang membuat hanya satu contender menjadi owner.
Desain relational dapat menempatkan record idempotency dan mutasi bisnis dalam satu transaction ketika keduanya berada di database yang sama.
BEGIN;
INSERT INTO idempotency_requests(account_id, key, status)
VALUES ($1, $2, 'in_progress');
INSERT INTO orders(account_id, ...)
VALUES ($1, ...);
UPDATE idempotency_requests
SET status = 'completed', response_code = 201, response_body = $3
WHERE account_id = $1 AND key = $2;
COMMIT;Unique constraint pada (account_id, key) menolak owner kedua. Bentuk schema dapat berbeda, tetapi invariant-nya tetap: dua contender tidak boleh melewati side-effect boundary secara independen untuk dedupe identity yang sama.
Pemakaian ulang key dengan input berbeda harus ditolak
Satu key tidak boleh diam-diam menjadi alias bagi dua operasi berbeda. Server dapat menyimpan fingerprint request yang canonical bersama key lalu membandingkan submission berikutnya terhadap nilai tersebut.
key: 8b7a...
fingerprint: SHA-256(canonical operation fields)Hanya field yang mendefinisikan operasi logis yang perlu masuk ke fingerprint. Metadata transport seperti tracing header biasanya tidak termasuk.
Canonicalization harus deterministik. Hash terhadap byte JSON mentah dapat menganggap perbedaan formatting atau urutan object key sebagai request berbeda. Representasi canonical yang terstruktur, atau pilihan field ternormalisasi yang ditetapkan aplikasi, menghindari ambiguitas tersebut.
Jika key yang sudah ada datang dengan fingerprint berbeda, hasil yang aman adalah conflict response, bukan replay outcome milik operasi lain.
Attempt yang masih berjalan memerlukan policy eksplisit
Retry concurrent dapat tiba ketika request pertama masih memiliki key. Cached result belum tersedia karena hasil final belum terbentuk.
Service dapat menunggu owner dalam waktu singkat, mengembalikan conflict yang dapat di-retry, atau menyediakan operation status resource. Pilihannya bergantung pada latency budget dan semantik API, tetapi request kedua tidak boleh mendapat izin untuk menjalankan mutasi.
key tidak ada -> claim dan execute
key in_progress -> wait, conflict, atau laporkan pending
key completed -> replay outcome tersimpanRecord in_progress juga dapat bertahan setelah worker crash. Recovery membutuhkan state durable yang cukup untuk membedakan attempt terbengkalai sebelum commit dari mutasi yang sudah commit di tempat lain. Menghapus row in-progress yang lama begitu saja dapat berbahaya jika side effect mungkin sudah terjadi.
Outcome tersimpan merupakan bagian dari kontrak
Untuk key yang sudah selesai, replay biasanya mengembalikan outcome operasi logis pertama alih-alih menjalankan handler lagi. Menyimpan marker seperti done=true saja mungkin tidak cukup ketika client memerlukan identifier resource yang dibuat, response status, atau hasil stabil lainnya.
Tidak semua byte respons harus disimpan. Record dapat menyimpan resource ID lalu membangun representasi terbaru jika kontrak API mengizinkannya. API lain mungkin perlu melakukan replay terhadap status awal dan field respons tertentu.
Error juga memerlukan klasifikasi. Validation error sebelum side effect biasanya dapat dikembalikan tanpa mengonsumsi key. Failure deterministik setelah key diklaim mungkin layak dicatat. Error infrastruktur transient sebelum commit dapat membuat operasi tetap memenuhi syarat untuk attempt berikutnya. State tersebut perlu mengikuti commit boundary service yang sebenarnya, bukan satu aturan umum untuk semua respons non-2xx.
Side effect eksternal memerlukan protocol yang lebih luas
Database transaction tidak dapat mencakup payment processor, email provider, atau message broker yang terpisah secara atomik kecuali sistem tersebut ikut dalam transaction protocol yang sama. Menandai key selesai sebelum effect eksternal dapat menghilangkan effect; menjalankan effect lebih dulu lalu crash sebelum mencatat completion dapat membuat retry mengulanginya.
Boundary karena itu perlu diperluas melalui mekanisme lain. Outbox dapat mencatat mutasi lokal dan event secara atomik, lalu mengirim event tersebut secara terpisah. Downstream API dapat menerima identity idempotency yang sama. Workflow dapat menyimpan state setiap step dan membuat tiap step yang terlihat dari luar aman terhadap duplikasi.
API request
|
+--> local transaction
|-- idempotency record
|-- business state
`-- outbox event
|
v
async deliveryDuplicate suppression end-to-end hanya sekuat side-effect boundary yang paling sempit. HTTP endpoint yang terlindungi tidak mencegah downstream effect ganda jika worker membuang identity operasi.
Idempotency bukan exactly-once transport
Network dapat menggandakan, menunda, dan menghilangkan message. Client dapat melakukan retry setelah timeout. Worker dapat restart setelah progress parsial. Idempotency key tidak menghapus perilaku tersebut dan tidak menciptakan exactly-once delivery.
Mekanisme ini menyediakan identity stabil agar service dapat membuat delivery berulang mengarah ke satu logical effect dalam scope dan retention period yang ditetapkan. Transport tetap dapat mengirim beberapa attempt.
Batas ini menjaga ekspektasi operasional tetap presisi. Metric sebaiknya membedakan logical operation dari physical attempt serta menampilkan key conflict, tabrakan in-progress, jumlah replay, retry setelah key kedaluwarsa, dan failure di sekitar commit boundary.
Guarantee yang berguna bersifat sempit dan dapat diuji
Implementasi yang baik dapat menyatakan guarantee secara konkret: untuk caller scope, idempotency key, operation fingerprint yang cocok, dan retention window yang didukung, retry concurrent maupun sequential tidak menjalankan mutasi terlindungi secara independen.
Guarantee tersebut bergantung pada ownership atomik, state operasi durable, binding payload yang ketat, recovery yang disengaja untuk pekerjaan yang belum selesai, serta propagasi identity operasi melewati setiap side-effect boundary yang memerlukan duplicate suppression.
Dengan komponen tersebut, retry menjadi mekanisme recovery untuk outcome yang ambigu tanpa mengubah respons yang hilang menjadi mutasi kedua yang tidak disengaja.