Idempotency Key Membuat Retry Write Aman Diulang
Client dapat kehilangan response dari write yang sebenarnya sudah berhasil. Koneksi mungkin terputus setelah server mencatat pembayaran, membuat order, atau menjadwalkan job, tetapi sebelum response sampai ke caller. Dari sisi client, kegagalan dan keberhasilan dapat terlihat sama.
Retry tanpa kontrol berbahaya untuk operasi dengan efek non-idempotent. Mengirim POST yang sama dua kali dapat membuat dua resource atau mengenakan charge dua kali. Tidak melakukan retry juga menyisakan outcome yang ambigu bagi caller.
Idempotency key memberi identitas stabil pada percobaan yang berulang. Client membuat satu key untuk satu operasi logis dan mengirim key yang sama pada setiap retry. Server mencatat outcome yang terkait dengan key tersebut lalu dapat mengembalikan outcome itu tanpa menjalankan efek yang sama lagi.
Key mengidentifikasi operasi, bukan percobaan transport
Percobaan transport baru harus memakai kembali key yang sudah ada ketika tujuannya tetap mewakili write yang sama.
percobaan 1:
POST /payments
Idempotency-Key: 7f3a...
koneksi terputus sebelum response
percobaan 2:
POST /payments
Idempotency-Key: 7f3a...Request kedua bukan pembayaran baru. Request itu merupakan percobaan lain untuk memperoleh hasil pembayaran yang diidentifikasi oleh 7f3a....
Pembayaran yang benar-benar baru memerlukan key baru. Memakai kembali satu key untuk operasi yang tidak berkaitan akan melebur write yang berbeda menjadi satu identitas dan dapat mengembalikan hasil milik request sebelumnya.
Karena itu, lifecycle key merupakan kontrak di sisi client sekaligus mekanisme di sisi server.
Deduplication harus atomic dengan kepemilikan operasi
Server tidak dapat menerapkan idempotency secara aman dengan check longgar yang diikuti insert:
if key does not exist:
perform side effect
insert keyDua request concurrent dapat sama-sama melihat bahwa key belum ada, lalu keduanya menjalankan side effect.
Sistem memerlukan transisi atomic yang memberikan kepemilikan key kepada satu request. Unique constraint pada database merupakan building block yang umum:
INSERT INTO idempotency_records (scope, key, status)
VALUES ('account-42', '7f3a...', 'in_progress');Unique index pada (scope, key) hanya mengizinkan satu insert berhasil. Request lain dapat memeriksa record yang sudah ada lalu mengikuti policy untuk operasi yang masih berjalan atau sudah selesai.
Batas transaction yang tepat bergantung pada lokasi business effect. Jika effect dan idempotency record berada dalam satu transactional database, keduanya sering dapat di-commit bersama. Jika effect melintasi boundary service eksternal, unique constraint lokal saja tidak dapat membuat remote side effect menjadi atomic.
Parameter request merupakan bagian dari identitas yang disimpan
Sebuah key tidak seharusnya diam-diam mengizinkan payload apa pun. Jika caller mengirim key yang sama dengan parameter berbeda, memperlakukan kedua request sebagai operasi setara dapat menyembunyikan defect pada client atau mengembalikan hasil dari operasi yang salah.
Record yang praktis menyimpan fingerprint kanonis dari field request yang mendefinisikan operasi:
key: 7f3a...
fingerprint: sha256(canonical operation fields)
status: completed
response: 201 + resource referencePada retry, server membandingkan fingerprint yang masuk dengan nilai tersimpan. Mismatch sebaiknya menghasilkan conflict eksplisit, bukan menjalankan operasi kedua atau mengembalikan hasil yang tidak berkaitan.
Canonicalization harus stabil. Melakukan hash langsung terhadap byte JSON sering tidak cocok karena JSON yang secara semantik setara dapat berbeda pada whitespace, urutan member object, atau detail serialization. Service perlu menetapkan field dan encoding yang ikut membentuk fingerprint.
Request yang masih berjalan memerlukan response policy yang jelas
Retry dapat tiba ketika percobaan pertama masih berjalan. Pada saat itu belum ada response selesai yang dapat diputar ulang.
Beberapa policy dapat digunakan:
A. tunggu percobaan pertama, lalu replay hasilnya
B. kembalikan conflict atau status retryable selama pekerjaan berlangsung
C. tampilkan state operasi melalui status resource terpisahPilihan bergantung pada durasi request, arsitektur server, dan perilaku timeout client. Hal yang penting adalah request kedua tidak memulai effect yang sama secara independen.
Menunggu dapat menyederhanakan perilaku client untuk operasi singkat, tetapi memakai kapasitas request. Mengembalikan response in-progress menjaga request handling tetap terbatas, tetapi client harus melakukan retry atau polling sesuai kontrak yang didokumentasikan.
Hasil failure perlu disimpan secara disengaja
Tidak semua failure memiliki semantik replay yang sama. Validation error sebelum side effect biasanya aman dikembalikan secara konsisten. Infrastructure failure sementara sebelum kepemilikan operasi diperoleh dapat mengizinkan eksekusi baru. Failure setelah side effect eksternal dapat membuat outcome operasi tidak pasti.
State machine idempotency sebaiknya membedakan kondisi tersebut, bukan memperlakukan semua response non-2xx dengan cara yang sama.
Contohnya:
reserved -> executing -> completed
\
-> indeterminateState indeterminate berguna ketika service tidak dapat membuktikan apakah downstream effect sudah terjadi. Menjalankan effect lagi secara otomatis akan mengubah ketidakpastian menjadi kemungkinan duplikasi.
Recovery path dapat melakukan query ke downstream system memakai operation identifier miliknya, menjalankan reconciliation secara asynchronous, atau memerlukan intervensi operator untuk kasus langka. Tindakan yang tepat mengikuti guarantee dari downstream API.
Retention menentukan retry window
Idempotency record tidak selalu dapat disimpan selamanya. Service umumnya menghapus record setelah retention period yang didokumentasikan.
Expiration menciptakan boundary semantik. Setelah record dihapus, key yang sama tidak lagi dapat membuktikan keberadaan operasi sebelumnya. Retry yang sangat terlambat dapat dieksekusi lagi kecuali business constraint lain yang lebih durable mencegahnya.
Retention period sebaiknya lebih panjang daripada retry horizon maksimum yang dijanjikan API. Client SDK tidak seharusnya terus melakukan retry dengan key yang sama melewati kontrak tersebut seolah deduplication bersifat permanen.
Untuk operasi bernilai tinggi, business object dapat menyediakan uniqueness rule yang bertahan lebih lama. Order reference, transfer identifier, atau ledger entry dapat tetap unique setelah transport-level idempotency record kedaluwarsa.
Scope mencegah caller yang tidak berkaitan berbagi key space
Random key memiliki probabilitas collision yang rendah, tetapi scope di sisi server tetap penting. Identitas efektif sering berupa tuple seperti:
(tenant_id, endpoint_family, idempotency_key)Scoping mencegah satu tenant mengambil key yang kebetulan dipakai tenant lain dan memungkinkan opaque value yang sama valid pada namespace yang independen.
Scope sebaiknya berasal dari authenticated server-side context jika memungkinkan. Mempercayai tenant identifier yang dikirim caller tanpa authorization dapat mengubah deduplication store menjadi mekanisme interferensi lintas tenant.
Key juga perlu memiliki batas panjang dan representasi terbatas agar tidak menciptakan index entry berlebihan atau biaya storage yang patologis.
Idempotency tidak otomatis memberikan eksekusi exactly-once
Istilah “exactly once” mencakup beberapa guarantee yang berbeda. Idempotency key dapat membuat submission API berulang menuju satu operasi logis, tetapi tidak otomatis membuat setiap downstream action dieksekusi tepat satu kali.
Jika service menulis row database dan memublikasikan message dalam langkah terpisah, crash masih dapat terjadi di antara keduanya. Transactional outbox pattern, downstream deduplication, atau operation identifier mungkin tetap diperlukan pada boundary berikutnya.
Guarantee yang berguna lebih sempit dan lebih mudah diverifikasi: untuk scope dan retention window yang ditetapkan, request berulang dengan key yang sama serta parameter operasi yang setara tidak secara independen menciptakan protected effect.
Guarantee tersebut sebaiknya dicantumkan pada dokumentasi API bersama retention period, perilaku conflict, perilaku in-progress, dan aturan replay response.
Observability sebaiknya mengikuti operasi logis
Retry dapat membuat request-level metric menjadi bising. Lima percobaan HTTP dapat mewakili satu logical write.
Log dan trace sebaiknya membawa idempotency key atau derived identifier yang aman agar operator dapat mengorelasikan percobaan tanpa menganggap masing-masing sebagai business action independen. Metric dapat memisahkan transport attempt, deduplicated retry, key conflict, in-progress collision, dan logical operation yang selesai.
Raw key dapat bersifat sensitif jika client memasukkan business identifier walaupun panduan API meminta opaque random value. Logging policy karena itu tidak boleh menganggap setiap key sebagai metadata yang aman.
Kontrak idempotency yang terdefinisi dengan baik mengubah retry jaringan yang ambigu menjadi keputusan protocol yang deterministik. Client mempertahankan identitas operasi yang dimaksud, sedangkan server mengambil identitas itu secara atomic, memvalidasi parameter berulang, dan memakai kembali outcome yang tercatat. Hasilnya bukan primitive exactly-once universal, melainkan boundary presisi yang membuat retried write aman pada bagian yang memang dapat ditegakkan service.