Idempotency Key Membuat Retry Mutasi Tetap Aman

Client dapat kehilangan hasil mutasi yang sukses tanpa kehilangan mutasinya. Server mungkin sudah melakukan commit untuk pembayaran, reservasi, atau pengiriman job lalu koneksi terputus sebelum response sampai ke caller. Dari sisi client, timeout menyisakan dua kemungkinan: operasi gagal sebelum commit, atau commit sudah terjadi dan hanya response yang hilang.

Retry secara buta pada mutasi non-idempotent dapat menerapkan efek dua kali. Menolak setiap retry membuat caller tidak memiliki jalur pemulihan dari hasil yang ambigu. Idempotency key memberi identitas stabil untuk satu operasi logis, sehingga percobaan berulang dapat memakai hasil dari percobaan pertama yang diterima alih-alih membuat efek baru.

Key tersebut bukan mekanisme retry. Fungsinya adalah kontrak deduplikasi di sekitar mutasi.

Satu operasi logis memakai satu key

Client membuat key sebelum percobaan pertama dan memakai key yang sama persis untuk retry dari mutasi logis yang sama.

attempt 1
POST /charges
Idempotency-Key: 8f6c...
        |
        +--- response hilang

attempt 2
POST /charges
Idempotency-Key: 8f6c...
        |
        +--- operasi logis yang sama

Operasi logis baru memerlukan key baru. Pemakaian ulang key untuk mutasi yang tidak berkaitan dapat menyatukan pekerjaan berbeda menjadi satu hasil tercatat. Membuat key baru pada setiap retry juga menggagalkan deduplikasi karena server melihat setiap percobaan sebagai operasi independen.

UUID acak sering dipakai sebagai material key karena mudah dibuat dan risiko collision sangat kecil pada skala aplikasi biasa. Protocol tetap perlu menetapkan panjang maksimum, himpunan karakter yang diterima, dan scope.

Server mengikat key ke identitas request

Key saja tidak cukup. Jika client tanpa sengaja mengirim key yang sama dengan parameter berbeda, mengembalikan hasil pertama dapat menyembunyikan bug pada client.

Server dapat menyimpan fingerprint dari request yang sudah dinormalisasi bersama key:

key
request fingerprint
status
response metadata
created_at
expires_at

Pada request berulang, server membandingkan mutasi yang masuk dengan identitas tersimpan. Request yang cocok dapat menerima outcome tercatat. Request yang bertentangan sebaiknya ditolak, bukan diam-diam dianggap setara.

Normalisasi harus stabil. Hash atas byte JSON mentah dapat menganggap dua object yang secara semantik sama sebagai berbeda ketika urutan field atau whitespace yang tidak signifikan berubah. Sistem dapat membuat fingerprint dari field kanonis terpilih atau memakai serialisasi deterministik yang ditetapkan oleh kontrak API.

Material request yang sensitif tidak perlu disalin ke tabel deduplikasi. Digest atau identifier terpilih dapat mengikat request sambil mengurangi data yang disimpan.

Claim key dan commit efek harus selaras

Race utama muncul saat dua percobaan dengan key yang sama tiba hampir bersamaan.

request A ----\
               +--> key yang sama
request B ----/

Urutan read-then-write tanpa uniqueness constraint tidak aman. Kedua worker dapat melihat bahwa record belum ada, keduanya menjalankan mutasi, lalu baru mencoba menyimpan key.

Claim deduplikasi memerlukan boundary atomic. Implementasi berbasis database dapat memakai unique constraint pada scoped key dan membuat idempotency record dalam transaction yang sama dengan mutasi bisnis jika keduanya berada dalam satu transactional database.

BEGIN;

INSERT INTO idempotency_records (scope, key, request_hash, state)
VALUES ('account-42', '8f6c...', 'sha256:...', 'started');

-- terapkan mutasi bisnis di sini

UPDATE idempotency_records
SET state = 'completed', status_code = 201
WHERE scope = 'account-42' AND key = '8f6c...';

COMMIT;

Unique constraint menentukan percobaan concurrent yang memiliki hak eksekusi pertama. Percobaan lain harus memeriksa record yang sudah ada dan tidak melanjutkan mutasi.

Jika efek bisnis dan idempotency record berada di sistem transactional yang berbeda, satu local transaction tidak dapat membuat kedua commit menjadi atomic. Arsitektur tersebut memerlukan model koordinasi eksplisit, bukan asumsi bahwa key menghilangkan dual-write gap.

Request yang masih berjalan memerlukan response yang jelas

Duplikat dapat tiba ketika percobaan pertama masih memiliki key tetapi belum selesai. Cached success belum dapat dikembalikan karena hasil final belum tersedia.

Beberapa policy dapat dipakai:

existing state = started
    |
    +--> tunggu completion
    +--> return conflict / retryable status
    +--> gabung ke shared in-flight work

Pilihan bergantung pada durasi request, arsitektur server, dan semantics API. Menunggu memakai resource dan memerlukan deadline. Mengembalikan response segera memindahkan pengaturan waktu retry ke client. Bergabung ke in-flight work memerlukan koordinasi lokal atau terdistribusi jika caller dapat mencapai instance berbeda.

Worker yang crash juga dapat meninggalkan record started. Sistem memerlukan state yang cukup untuk membedakan ownership aktif dari pekerjaan yang terbengkalai, dan recovery harus sesuai dengan semantics commit mutasi bisnis. Menghapus semua row started yang lama lalu mengeksekusi ulang tidak aman jika efek mungkin sudah ter-commit.

Outcome tercatat merupakan bagian dari kontrak

Deduplikasi paling kuat ketika request berulang menerima outcome eksternal yang sama dengan request pertama yang selesai. Ini biasanya berarti menyimpan status code dan response data secukupnya untuk membentuk kembali reply.

first attempt:
  execute -> 201 + resource_id=abc

retry:
  lookup  -> 201 + resource_id=abc

Tidak semua response perlu disimpan selamanya atau secara penuh. Body besar dapat diganti dengan resource reference jika API dapat membentuk response yang setara. Header yang sensitif dan metadata transport sementara biasanya tidak perlu masuk ke record.

Failure policy juga perlu presisi. Validation error yang muncul sebelum mutasi dapat aman dihitung ulang. Failure yang dikembalikan setelah commit mungkin perlu dicatat karena menjalankan eksekusi lagi dapat menggandakan efek. Aturan penyimpanan sebaiknya mengikuti titik saat operasi menjadi committed secara eksternal.

Scope mencegah collision antar-client

Key tekstual yang sama dapat digunakan dengan aman pada namespace terpisah jika storage key menyertakan owner yang relevan:

(account_id, idempotency_key)

Scope dapat mengikuti account, tenant, API credential, endpoint, atau boundary kontrak lain. Namespace global sederhana, tetapi key yang dipilih satu client dapat bertabrakan dengan request client lain kecuali key sudah acak secara kriptografis dan dipisahkan oleh authorization.

Authorization tetap berlaku pada setiap percobaan. Kepemilikan idempotency key yang valid tidak boleh memberikan akses ke recorded response milik principal lain.

Expiration menetapkan retry window

Idempotency record umumnya tidak dapat tumbuh tanpa batas. Retention period membuat deduplication window menjadi terbatas.

Setelah expiration, key yang sama dapat dianggap baru. API karena itu perlu menyatakan retry horizon yang didukung. Cleanup juga harus memperhitungkan in-progress record dan business reference yang diperlukan untuk membentuk response.

Retention merupakan tradeoff operasional. Window yang lebih panjang memakai lebih banyak storage tetapi mencakup retry yang terlambat. Window yang lebih pendek mengurangi state tetapi membuat retry lama dapat dieksekusi kembali. Durasi yang tepat mengikuti interval retry maksimum yang siap didukung service.

Idempotency tidak membuat semua retry aman

Key hanya melindungi operasi yang berada dalam boundary deduplikasinya. Side effect di luar boundary tersebut masih dapat terduplikasi.

Sebagai contoh, transaction dapat membuat order lalu mengirim email setelah commit. Jika process crash di antara dua langkah tersebut, replay request dari idempotency record yang sudah completed tidak seharusnya mengirim email lagi sebagai efek samping request handling. Pengiriman side effect yang durable memerlukan identity dan delivery policy sendiri.

Pemisahan yang sama berlaku untuk panggilan ke service lain. Pencatatan hasil idempotency lokal tidak secara retroaktif melakukan deduplikasi terhadap remote mutation yang dikirim tanpa operation identity yang kompatibel.

Metrics menampilkan celah pada kontrak

Telemetry yang berguna memisahkan first attempt, completed replay, conflicting payload, in-progress duplicate, expired key, dan storage failure. Setiap kategori menunjukkan masalah yang berbeda.

Replay count yang meningkat dapat berasal dari perilaku retry client yang normal. Payload conflict dapat menunjukkan client memakai key secara keliru. In-progress record yang menetap dapat menandakan worker macet atau recovery yang tidak lengkap. Storage error perlu perhatian khusus karena melanjutkan operasi tanpa deduplication claim dapat mengubah kegagalan infrastruktur menjadi efek bisnis ganda.

Log sebaiknya menyertakan identifier key yang aman atau digest, scope, dan state transition tanpa membuka request body yang sensitif.

Key mengubah ambiguitas menjadi lookup yang stabil

Kegagalan jaringan membuat outcome mutasi ambigu bagi caller. Idempotency key tidak menghapus ambiguitas dari transport; key menyediakan tempat durable di server untuk menyelesaikannya.

Guarantee yang berguna tetap sempit: dalam scope dan retention window yang didokumentasikan, percobaan berulang untuk operasi yang sama dapat berujung pada satu execution result yang tercatat. Guarantee tersebut bergantung pada atomic claiming, request binding, penanganan in-progress yang eksplisit, dan storage yang tetap tersedia sepanjang retry path.