Client dapat kehilangan response sebuah command meskipun server sudah menyelesaikan operasinya. Koneksi bisa terputus setelah pembayaran tercatat, job dibuat, atau order diterima. Dari sisi client, timeout tidak menunjukkan apakah command gagal sebelum dieksekusi atau berhasil sebelum response hilang.

Retry diperlukan untuk availability, tetapi mengulang command yang mengubah state dapat menggandakan side effect. Idempotency key memberi identitas yang sama pada seluruh percobaan untuk satu command logis. Server menyimpan hasil yang terkait dengan identitas tersebut dan menggunakannya kembali ketika command yang sama datang lagi.

Key tidak membuat kode arbitrer menjadi idempotent. Mekanisme ini membentuk boundary deduplikasi di sekitar command dan memerlukan aturan yang presisi untuk scope key, ekuivalensi request, percobaan concurrent, retention, dan pemulihan kegagalan.

Satu command logis memakai satu key

Client membuat key sebelum percobaan pertama dan mengirim nilai yang sama pada setiap retry command tersebut.

POST /payments
Idempotency-Key: 8f54b7d2-5a8e-4f48-9f13-9d44ec6b0d3a
Content-Type: application/json

{
  "order_id": "ord_123",
  "amount": 4200,
  "currency": "USD"
}

Jika response hilang, retry membawa key dan payload command yang sama. Pembayaran lain memakai key berbeda, bahkan ketika seluruh business field kebetulan identik.

Perbedaan ini penting. Deduplikasi yang hanya memakai order_id, amount, atau business field lain dapat menggabungkan operasi terpisah yang memang valid. Key khusus merepresentasikan identitas retry, bukan kemiripan yang disimpulkan server.

Key juga memerlukan scope. Service dapat menetapkan keunikan per account, API credential, endpoint, atau operation type. Identitas gabungan seperti (tenant_id, idempotency_key) mencegah benturan antar-tenant sekaligus menjaga lookup tetap berada pada security boundary yang relevan.

Hasil durable pertama menjadi hasil replay

Record sederhana dapat menyimpan identitas command dan informasi yang cukup untuk menghasilkan kembali outcome:

idempotency_key
request_fingerprint
status
response_code
response_body
resource_id
created_at
expires_at

Pada request pertama, server mereservasi key lalu menjalankan operasi. Setelah operasi mencapai durable commit point, record idempotency menyimpan outcome yang sudah selesai. Retry berikutnya mengembalikan hasil tersimpan tanpa menjalankan side effect lagi.

percobaan pertama:
key K -> reservasi K -> commit operasi -> simpan hasil R

retry:
key K -> temukan K completed -> kembalikan R

Response replay tidak harus identik byte demi byte jika kontrak API mengizinkan header atau metadata transport dibuat ulang. Properti utamanya adalah operasi logis tidak diterapkan dua kali dan status yang dikembalikan tetap konsisten dengan outcome commit awal.

Menyimpan resource identifier saja dapat memadai jika response bisa direkonstruksi dengan aman. Menyimpan response awal berguna ketika rekonstruksi dapat membaca state yang lebih baru dan menghasilkan representasi yang berbeda secara material.

Ekuivalensi request harus diperiksa

Sebuah key tidak boleh diam-diam mengizinkan command berbeda.

Misalnya client pertama kali mengirim:

{"order_id":"ord_123","amount":4200,"currency":"USD"}

lalu memakai kembali key yang sama dengan:

{"order_id":"ord_123","amount":8400,"currency":"USD"}

Mengembalikan hasil pertama menyembunyikan defect pada client. Menjalankan request kedua merusak deduplikasi. Kontrak yang lebih aman menolak pemakaian ulang key dengan parameter operasi yang berbeda.

Service sering menyimpan fingerprint dari canonical request field yang mendefinisikan operasi. Canonicalization harus dirancang secara sengaja. Hash atas raw JSON bytes dapat menganggap perbedaan format atau urutan object member sebagai request berbeda, sedangkan menghilangkan field yang bermakna dapat membuat dua command berbeda terlihat ekuivalen.

Authentication context dapat berada di luar atau di dalam fingerprint, bergantung pada scope key. Aturannya harus sesuai dengan model authorization dan tenancy API, bukan sekadar mengikuti resep hash generik.

Duplicate concurrent memerlukan satu pemenang

Retry tidak selalu berjalan berurutan. Client, proxy, atau job runner dapat mengirim percobaan yang overlap sebelum percobaan pertama selesai.

Urutan read lalu insert tidak aman:

A: lookup K -> tidak ada
B: lookup K -> tidak ada
A: jalankan side effect
B: jalankan side effect

Reservasi key memerlukan conflict boundary yang atomic, biasanya database unique constraint atau primitive compare-and-create lain.

INSERT INTO idempotency_keys (tenant_id, key, status)
VALUES ('tenant_7', '8f54b7d2...', 'in_progress')
ON CONFLICT (tenant_id, key) DO NOTHING;

Hanya request yang berhasil membuat reservasi yang melanjutkan proses sebagai owner. Request concurrent yang menemukan in_progress memerlukan response policy yang terdokumentasi: request dapat menunggu completion, melakukan polling pada internal state, atau menerima status yang dapat di-retry. Menjalankan command secara paralel bukan fallback yang valid jika duplicate effect dilarang.

Mekanisme reservasi dan side effect tetap membutuhkan strategi commit yang andal. Menang pada unique insert saja tidak menyelesaikan crash yang terjadi di antara reservasi key dan perubahan business state.

Atomic boundary menentukan perilaku saat crash

Desain paling kuat menempatkan record idempotency dan business mutation dalam transaksi database yang sama ketika keduanya berada pada satu transactional store.

BEGIN;

INSERT INTO idempotency_keys (...);

INSERT INTO payments (...);

UPDATE orders
SET payment_status = 'paid'
WHERE id = 'ord_123';

UPDATE idempotency_keys
SET status = 'completed',
    response_code = 201,
    resource_id = 'pay_456'
WHERE key = '8f54b7d2...';

COMMIT;

Rollback menghapus reservasi dan business mutation. Commit membuat keduanya terlihat bersama.

Ketika side effect melintasi transactional boundary, desain memerlukan mekanisme recovery lain. Memanggil provider eksternal setelah mereservasi key lokal dapat meninggalkan row in_progress jika proses berhenti. Service kemudian memerlukan durable state yang cukup untuk merekonsiliasi percobaan tersebut tanpa mengulang external effect yang statusnya belum pasti.

Ini adalah batas distributed system yang juga muncul pada dual-write problem lain: tabel deduplikasi tidak dapat menciptakan atomicity di antara sistem independen. Operasi eksternal mungkin memerlukan idempotency token sendiri, durable workflow, outbox-style handoff, atau reconciliation berdasarkan operation identifier yang stabil.

Failure response memerlukan policy eksplisit

Tidak setiap response harus disimpan di bawah key.

Validation error yang terdeteksi sebelum side effect dapat aman dikembalikan lagi, tetapi sebagian API mengizinkan caller memperbaiki request dan melakukan retry dengan key baru. Server failure sebelum command dimulai dapat membiarkan key tetap tidak terpakai. Failure setelah commit harus mempertahankan committed outcome meskipun serialisasi response atau pengiriman jaringan gagal.

Boundary yang berguna bukan sekadar HTTP 2xx versus 5xx. Penentunya adalah apakah command logis sudah melewati durable point yang tidak boleh dijalankan ulang.

Implementasi dapat memodelkan state seperti:

reserved -> completed
reserved -> failed_final
reserved -> recoverable

Nama state tidak sepenting transisi deterministik dan recovery rule. State ambigu yang membuat operator atau worker menjalankan side effect secara manual dapat melewati proteksi yang seharusnya diberikan key.

Retention menentukan jendela deduplikasi

Record idempotency biasanya tidak dapat disimpan selamanya. Periode retention menentukan interval ketika key yang diulang dijamin mengarah ke operasi sebelumnya.

Menghapus record terlalu cepat dapat mengubah retry yang terlambat menjadi command baru. Menyimpannya tanpa batas menambah storage, ukuran index, dan kewajiban data retention.

Kontrak API sebaiknya menyatakan masa berlaku key yang didukung ketika client dapat bergantung padanya. Cleanup harus memakai time semantics yang sama dengan lookup path, dan expiration tidak boleh beradu dengan operasi aktif. Record berstatus in_progress biasanya memerlukan penanganan berbeda dari completed record yang sudah lama.

Client retry policy dan server retention harus selaras. Client yang dapat melakukan retry selama 24 jam tidak dapat bergantung pada server yang melupakan key setelah 30 menit.

Key bukan authorization credential

Idempotency key mengidentifikasi percobaan command; key tidak boleh memberikan akses ke command atau hasilnya.

Server tetap harus menjalankan authentication dan authorization normal pada retry. Lookup key juga memerlukan isolasi tenant agar satu caller tidak dapat memeriksa outcome milik caller lain dengan menebak atau memperoleh sebuah key.

Key dengan entropy tinggi mengurangi benturan tidak sengaja dan risiko penebakan, tetapi entropy tidak menggantikan access control. Log dan metric juga tidak semestinya mengekspos request body sensitif hanya karena data tersebut melekat pada record deduplikasi.

Rate limit tetap relevan. Client dapat mengirim key yang sama berulang kali dan mengonsumsi kapasitas lookup, waiting, atau response bandwidth walaupun business mutation hanya berjalan sekali.

Observability perlu menampilkan state deduplikasi

Metric yang berguna memisahkan percobaan pertama dari replay dan conflict. Service dapat mencatat reservasi baru, completed replay, payload mismatch, hit in_progress concurrent, pemakaian ulang key yang expired, recovery action, dan usia reservasi unfinished yang paling lama.

Sinyal tersebut menunjukkan fault yang berbeda. Lonjakan replay dapat menandakan network instability atau client retry yang agresif. Banyak payload mismatch dapat menunjukkan client memakai key secara keliru. Reservasi unfinished yang tua mengarah pada crash recovery atau workflow yang macet.

Tracing sebaiknya mempertahankan logical operation identifier yang stabil pada seluruh retry sambil tetap memberikan request atau span identity berbeda untuk setiap transport attempt. Pemisahan ini membuat satu business command terlihat jelas tanpa melebur beberapa network attempt menjadi satu event.

Idempotency adalah kontrak di sekitar retry

Idempotency key mengubah retry yang tidak pasti dari “jalankan command ini lagi” menjadi “selesaikan outcome untuk identitas command ini.” Perubahan pada permukaan HTTP terlihat kecil, tetapi konsekuensinya pada storage semantics cukup besar.

Implementasi yang andal menetapkan scope key, membandingkan makna request, menserialisasi percobaan concurrent, menyelaraskan record key dengan business commit, mempertahankan record selama interval yang dinyatakan, dan memulihkan external effect yang ambigu tanpa pengulangan spekulatif.

Mekanisme ini paling berguna ketika retry memang diperkirakan dan duplicate side effect mahal. Idempotency key tidak menghapus distributed failure mode; key memberi identitas durable pada percobaan berulang sehingga service dapat membatasinya.