Idempotency Key Menyatukan Retry sebagai Satu Operasi Logis
Client dapat kehilangan response dari request yang sebenarnya berhasil. Server mungkin sudah melakukan commit untuk charge, membuat order, atau memasukkan job ke queue, lalu koneksi gagal sebelum response mencapai caller. Dari sisi client, hasil operasi menjadi ambigu.
Retry diperlukan untuk availability, tetapi retry biasa dapat mengulangi side effect. Idempotency key memberi cara bagi client untuk menyatakan bahwa beberapa percobaan HTTP mewakili satu operasi logis.
Server mencatat hasil pertama yang diterima dengan key tersebut. Percobaan berikutnya dengan key yang sama dapat menerima hasil yang sudah tercatat tanpa menjalankan operasi sekali lagi.
Hasil ambigu adalah failure mode utama
Perhatikan request yang membuat payment:
client API database
| | |
| POST /payments | |
| key: p-481 | |
|-------------------->| INSERT payment |
| |-------------------->|
| | commit |
| |<--------------------|
| connection lost |
x<--------------------| |Client tidak dapat menyimpulkan dari koneksi yang putus apakah database sudah melakukan commit. Mengirim business payload yang sama dengan identity baru dapat membuat payment kedua.
Idempotency key yang stabil membawa identity operasi logis melintasi setiap percobaan:
attempt 1: key=p-481 -> execute -> payment 901
attempt 2: key=p-481 -> replay -> payment 901Key tidak membuat jaringan menjadi andal. Key membuat repeated delivery aman pada application boundary yang menegakkannya.
Scope key adalah bagian dari kontrak API
Sebuah key harus unik dalam scope yang ditentukan. Scope itu dapat berupa account, tenant, API credential, endpoint, atau operation type.
Key mentah seperti p-481 jarang cukup sebagai storage identity. Server dapat membentuk composite identity:
tenant_id + operation_name + idempotency_keyScoping mencegah client yang tidak berkaitan mengalami collision pada key yang sama dan memungkinkan service menerapkan retention rule dengan namespace yang presisi.
Client sebaiknya membuat key sebelum percobaan pertama dan menggunakannya kembali pada setiap retry untuk operasi tersebut. Key baru pada setiap retry menghilangkan fungsi deduplication.
Reservation dan side effect memerlukan satu correctness boundary
Urutan yang naif masih memiliki race:
1. check that key is absent
2. perform side effect
3. store key and resultDua request concurrent dapat sama-sama melewati langkah 1 sebelum salah satunya menyimpan record. Keduanya kemudian dapat menjalankan operasi.
Untuk side effect yang tersimpan pada relational database yang sama, unique constraint dan transaction dapat membuat reservation serta mutation menjadi atomic. Salah satu pola adalah memasukkan idempotency record dengan unique composite key, menjalankan business mutation, menyimpan response metadata, lalu melakukan commit bersama.
CREATE UNIQUE INDEX idempotency_once
ON idempotency_records (tenant_id, operation, key);Jika business effect berada di luar transaction tersebut, desain memerlukan protocol lain. Idempotency row lokal tidak dapat secara atomic mencakup arbitrary remote API call. Durable state machine, provider-side idempotency, transactional messaging, atau reconciliation mungkin diperlukan sesuai boundary.
Key yang sama tidak boleh mengizinkan request berbeda
Bug pada client dapat menggunakan key yang sama untuk payload berbeda. Mengembalikan hasil pertama tanpa memeriksa request identity dapat menyembunyikan error dan mengaitkan caller dengan operasi yang tidak dimaksudkan.
Server dapat menyimpan canonical request fingerprint bersama key:
key: p-481
request_hash: sha256(canonical relevant fields)
status: completed
response_status: 201
resource_id: 901Request berikutnya dengan key dan fingerprint yang sama adalah retry. Key yang sama dengan fingerprint berbeda sebaiknya menghasilkan conflict, bukan menjalankan operasi atau melakukan replay secara diam-diam.
Canonicalization perlu didefinisikan dengan cermat. Hash terhadap raw JSON bytes menganggap perubahan formatting atau object-key order yang tidak bermakna sebagai input berbeda. Service dapat melakukan fingerprint terhadap semantic fields yang menentukan operasi dengan representation yang deterministic.
Percobaan yang masih berjalan memerlukan state eksplisit
Concurrent retry dapat tiba ketika percobaan pertama masih berjalan. Karena itu, idempotency record sering memerlukan state seperti in_progress, completed, dan failed.
Request kedua yang menemukan in_progress tidak boleh memulai salinan operasi lain. Bergantung pada kontrak API, request tersebut dapat menunggu, menerima conflict atau retryable response, atau melihat operation status melalui resource terpisah.
State transition juga harus bertahan saat process crash. Jika worker mati setelah melakukan reservation terhadap key, row in_progress permanen dapat memblokir operasi selamanya. Recovery dapat memakai lease, attempt epoch, timeout bersama reconciliation, atau queue dengan ownership rule yang memungkinkan takeover secara aman.
Recovery rule harus sesuai dengan side effect. Menghapus masa berlaku idempotency row lalu menjalankan operasi kembali secara buta tidak aman jika percobaan sebelumnya mungkin sudah melakukan commit di tempat lain.
Replay semantics perlu ditentukan secara sengaja
Completed record dapat menyimpan informasi yang cukup untuk mereproduksi kontrak dari percobaan sukses pertama. Informasi itu dapat berupa HTTP status, response fields tertentu, dan identifier resource yang dibuat.
Menyimpan seluruh response memang sederhana, tetapi dapat mempertahankan data sensitif atau berukuran besar. Menyimpan resource identifier saja mengurangi storage, tetapi response yang direkonstruksi kemudian dapat mencerminkan state yang sudah berubah sejak call pertama.
API perlu memilih semantics yang dijanjikan. Replay dapat berarti “mengembalikan response asli” atau “mengembalikan current representation dari resource asli”, tetapi keduanya merupakan kontrak berbeda.
Failure memerlukan perhatian serupa. Validation error yang terjadi sebelum side effect dapat aman untuk diulang tanpa persistence. Failure yang tercatat setelah partial external work mungkin memerlukan perlakuan durable. Rule yang menyimpan setiap error atau tidak menyimpan error sama sekali biasanya terlalu kasar.
Retention menentukan deduplication window
Idempotency record menggunakan storage, sehingga service umumnya menghapusnya setelah periode tertentu. Expiration juga membatasi periode ketika repeated key masih dikenali.
Jika record dipertahankan selama 24 jam, retry setelah window tersebut dapat diperlakukan sebagai operasi baru. Client memerlukan kontrak yang menyatakan boundary ini secara eksplisit jika delayed retry mungkin terjadi.
Retention sebaiknya mengikuti retry dan reconciliation horizon terpanjang yang memang didukung sistem, ditambah operational margin. Menghapus record lebih cepat hanya demi mengecilkan table dapat membuka kembali duplicate side effect.
Cleanup juga perlu menghindari penghapusan active record. Background job dapat menghapus completed record yang melewati retention deadline sambil mempertahankan active atau quarantined state sampai recovery path selesai.
Idempotency lebih sempit daripada exactly-once execution
Idempotency key tidak membuktikan bahwa code hanya dieksekusi satu kali. Handler dapat berjalan lebih dari sekali sambil menghasilkan satu durable effect yang diterima, atau crash di antara external effects yang tidak berbagi transaction.
Guarantee yang berguna lebih sempit: dalam scope dan retention window yang didokumentasikan, percobaan berulang dengan valid key yang sama diselesaikan sebagai satu operasi logis pada protected boundary.
Pembedaan ini menjaga desain tetap konkret. Key mengidentifikasi intent, durable state mencatat progresnya, atomic enforcement mencegah concurrent duplication, request fingerprint menolak conflicting reuse, dan retention menentukan berapa lama guarantee tetap tersedia.