Client dapat mengirim operasi logis yang sama lebih dari sekali meski hanya menginginkan satu efek. Timeout setelah POST /payments meninggalkan batas yang ambigu: server mungkin sudah melakukan commit pembayaran sementara client tidak menerima response. Retry memulihkan pengiriman, tetapi retry biasa dapat membuat pembayaran kedua. Idempotency key mengubah interface dengan memberi identitas request yang stabil pada percobaan berulang.

Key tersebut bukan pengganti transactionality dan tidak membuat setiap operasi secara intrinsik idempotent. Key membentuk protokol antara client dan server: percobaan dengan key yang sama diperlakukan sebagai kandidat untuk operasi logis yang sama. Server tetap memerlukan aturan mengenai ekuivalensi request, kedatangan konkuren, masa persistensi, pemulihan kegagalan, dan replay response.

Key mengidentifikasi operasi, bukan percobaan transport

Retry sering membuat koneksi TCP, objek HTTP request, trace span, atau eksekusi proses yang baru. Identitas pada lapisan transport tersebut tidak dapat mewakili operasi logis karena setiap percobaan dapat berbeda pada lapisan itu.

Key yang dibuat aplikasi tetap stabil di seluruh percobaan:

POST /payments
Idempotency-Key: 7c7c2f0e-6f39-4d57-8cb4-95ec3d2c4f51
Content-Type: application/json

{"account":"A17","amount":2500,"currency":"USD"}

Jika response hilang, client mengirim operasi yang sama dengan key yang sama. Pembayaran baru yang dimulai kemudian memperoleh key berbeda meski payload-nya kebetulan identik.

Pemisahan ini menghindari penggunaan kesamaan payload sebagai satu-satunya aturan identitas. Dua operasi yang sah dapat memiliki body identik. Sebaliknya, key yang digunakan kembali dengan body berbeda menandakan konflik protokol, bukan operasi valid kedua.

Pencocokan request menutup kasus penggunaan ulang key

Server yang hanya menyimpan key -> response dapat menerima penggunaan ulang key untuk request berbeda tanpa mendeteksinya. Record yang lebih aman mengikat key ke informasi request yang cukup untuk mendeteksi ketidakcocokan tersebut.

Secara konseptual, record dapat memuat:

key
request_fingerprint
state
status_code
response_body
created_at

Fingerprint dapat mencakup HTTP method, identitas route, dan representasi kanonis dari field yang menentukan operasi. Komposisi persisnya merupakan keputusan desain API. Header atau field yang tidak memengaruhi semantik operasi tidak harus disertakan, sedangkan menghilangkan field yang relevan secara semantik dapat membuat request berbeda terlihat ekuivalen.

Saat key muncul kembali, server membandingkan request masuk dengan fingerprint tersimpan. Ketidakcocokan sebaiknya menghasilkan response konflik alih-alih melakukan replay hasil yang dibuat untuk input berbeda.

Canonicalization juga memerlukan kontrak yang jelas. Hash terhadap byte JSON mentah membuat whitespace dan urutan member object menjadi signifikan meski aplikasi menganggapnya ekuivalen. Hash terhadap bentuk kanonis setelah parsing dapat menghindari perbedaan itu, tetapi aturan canonicalization kemudian menjadi bagian dari implementasi server.

Duplikat konkuren memerlukan atomic claim

Deteksi duplikat secara berurutan tidak cukup. Dua request dengan key yang sama dapat tiba hampir bersamaan:

request A ---- check: absent ---- execute
request B ---- check: absent ---- execute

Keduanya melihat kondisi absent sebelum salah satunya menyimpan hasil. Tabel idempotency tidak mencegah eksekusi ganda.

Server memerlukan transisi atomik untuk melakukan claim terhadap key. Unique constraint pada database merupakan primitive yang umum:

INSERT INTO idempotency_records (key, request_fingerprint, state)
VALUES (?, ?, 'in_progress');

dengan unique constraint pada key, atau pasangan yang memiliki scope seperti (tenant_id, key). Tepat satu insert konkuren dapat berhasil. Request yang mengalami konflik kemudian membaca record yang ada dan mengikuti kebijakan API untuk operasi yang masih berjalan.

Kebijakan dapat berupa menunggu sampai selesai, mengembalikan status yang dapat di-retry, atau berkoordinasi melalui mekanisme sinkronisasi lain. Properti pentingnya adalah eksekusi duplikat tidak dapat dimulai hanya karena dua pemeriksaan kondisi absent mengalami race.

Scope uniqueness juga penting. Key space yang unik secara global dapat tidak diperlukan dan dapat menimbulkan coupling antar-tenant. Scope berdasarkan akun terautentikasi atau tenant sering lebih sesuai dengan batas operasi.

Efek bisnis dan record idempotency harus berbagi model kegagalan

Melakukan claim terhadap key sebelum operasi dieksekusi menambah batas kegagalan. Proses dapat crash setelah menandai record sebagai in_progress. Proses juga dapat melakukan commit mutasi bisnis lalu crash sebelum menyimpan response yang dapat di-replay.

Jika mutasi bisnis dan record idempotency berada dalam database transaksional yang sama, satu transaction sering dapat mengikat efek dengan state idempotency final:

begin transaction
  verify or claim key
  apply business mutation
  store completed result
commit

Susunan tersebut dapat mencegah efek bisnis yang sudah di-commit tanpa record completed yang sesuai, mengikuti semantik transaction database yang digunakan.

Batasnya lebih rumit ketika operasi memanggil service eksternal. Transaction lokal tidak dapat melakukan commit atomik atas row lokal dan API remote yang tidak terkait. Sistem kemudian memerlukan strategi kegagalan terdistribusi yang eksplisit. Service downstream dapat menerima idempotency key yang sama, atau caller dapat menyimpan operation state machine yang mampu merekonsiliasi hasil ambigu. Tabel idempotency saja tidak dapat menciptakan atomic commit lintas sistem independen.

Replay response merupakan bagian dari kontrak yang terlihat

Setelah operasi selesai, request duplikat biasanya tidak seharusnya menjalankan business action lagi. Request tersebut sebaiknya menerima response yang mewakili hasil tersimpan.

Salah satu bentuknya adalah menyimpan status code dan response body asli:

key -> 201, {"payment_id":"p_8042","status":"accepted"}

Menghitung ulang response dari state database saat ini tidak selalu ekuivalen. State dapat berubah sejak operasi awal. Pembayaran yang sebelumnya accepted mungkin kini settled atau refunded. Mengembalikan representasi terbaru mengubah hasil retry yang terlihat oleh client.

API dapat sengaja memilih kebijakan lain, tetapi kebijakan itu perlu eksplisit. Menyimpan response asli memberi semantik replay yang kuat dengan konsekuensi tambahan storage dan potensi sensitivitas data response yang disimpan.

Penanganan error juga membutuhkan batas. Validation failure yang terjadi sebelum operasi diterima dapat tidak disimpan sehingga request yang sudah dikoreksi dengan key baru dapat diproses. Failure setelah eksekusi dimulai dapat sangat berbeda karena retry mungkin mengulangi efek yang sudah terjadi sebagian. API harus menetapkan outcome mana yang menjadi record idempotency persisten.

Expiration membatasi jaminan

Record idempotency umumnya tidak dapat disimpan selamanya. Retention window membatasi biaya storage sekaligus durasi jaminan retry.

Misalnya record kedaluwarsa setelah 24 jam. Duplikat yang dikirim dalam rentang itu dapat dikenali. Key yang sama setelah record dihapus dapat terlihat sebagai operasi baru dan dieksekusi lagi. API dengan demikian tidak menawarkan exactly-once execution tanpa batas waktu; API menawarkan duplicate suppression dalam batas retention key yang dinyatakan.

Expiration juga memengaruhi client. Retry queue yang dapat offline lebih lama daripada retention period server tidak dapat mengasumsikan key lama masih akan menekan duplikat. Workflow berumur panjang mungkin memerlukan operation identifier persisten pada domain model, bukan hanya bergantung pada HTTP idempotency cache.

Penghapusan juga perlu menghormati operasi aktif. Menghapus record in_progress hanya berdasarkan waktu pembuatannya dapat membuka eksekusi kedua saat eksekusi pertama masih berjalan. Kebijakan cleanup perlu mempertimbangkan state operasi dan interval maksimum yang masuk akal untuk eksekusi atau pemulihan.

Idempotency lebih sempit daripada exactly-once delivery

Network dapat menduplikasi, menunda, atau menghilangkan message, dan client dapat kehilangan response. Protokol idempotency key menangani satu konsekuensi spesifik: percobaan berulang dapat dipetakan kembali ke satu operasi logis selama server masih menyimpan record terkait dan seluruh efek yang terlibat mematuhi kontrak kegagalan yang diperlukan.

Protokol ini tidak memaksa network mengirim tepat sekali. Protokol ini tidak membuat side effect eksternal menjadi transactional. Protokol ini tidak memperbaiki client yang membuat key baru untuk setiap retry. Perlindungan juga tidak berlaku setelah retention boundary terlewati.

Jaminan yang berguna karena itu dinyatakan pada batas API: untuk scope key, aturan ekuivalensi request, retention interval, dan execution path yang terdefinisi, request berulang yang cocok memakai kembali satu operation record alih-alih memulai business action secara independen. Pernyataan yang lebih sempit ini memperlihatkan mekanisme aktual yang harus tetap benar saat terjadi retry dan crash.