Idempotency
Ketika aplikasi frontend melakukan checkout ke aplikasi backend kita, ekspektasi kita adalah akan terbuat satu order saja. Tetapi kenyataannya, itu bisa saja tidak terjadi. Kenapa? karena bisa jadi ada hal-hal yang membuat request dari frontend itu gagal, misal karena di sisi frontend sudah timeout duluan karena backend tidak kunjung memberikan response. Meskipun terlihat gagal di sisi frontend, tapi sebenarnya request nya itu sudah diproses oleh backend. Karena frontend menganggap requestnya gagal, maka dari frontend akan melakukan retry untuk melakukan checkout dengan data yang sama. Hal ini akan berbahaya, karena bisa terbuat 2 order yang sama di backend jika kita tidak handle permasalahan ini dengan baik.
Untuk lebih menggambarkan masalah yang terjadi, mari saya coba gambarkan :
Untuk menangani masalah ini, kita bisa coba selesaikan dengan solusi yang dinamakan idempotency. Idempotency adalah konsep dalam sistem komputasi dimana melakukan operasi yang sama berkali-kali hanya menghasilkan efek sekali saja, tanpa efek samping tambahan. Untuk lebih jelasnya, saya coba gambarkan ketika kita melakukan call menggunakan HTTP Method :
- GET
Operasi GET dikatakan idempotent, karena tidak mengubah state apapun ketika dipanggil. Contoh :
GET /users/1232. PUT
Operasi PUT dikatakan idempotent, karena ketika ada proses melakukan update seluruh data hasilnya akan selalu sama. Contoh :
PUT /users/123
Body: { "name": "Deni", "role": "admin" }3. DELETE
Operasi DELETE dikatakan idempotent, karena ketika data dihapus dan proses delete itu diulangi maka tidak ada state yang berubah. Contoh :
DELETE /users/1234. POST
Get Solehudin MQ’s stories in your inbox
Join Medium for free to get updates from this writer.
Operasi POST dikatakan tidak idempotent, karena ketika membuat resource baru. Setiap request akan menghasilkan data baru. Contoh :
POST /users
Body: { "name": "Deni" }Karena HTTP method POST dianggap tidak idempotent, maka diperlukan cara khusus untuk membuatnya menjadi idempotent. Untuk melakukannya biasanya ketika frontend melakukan checkout, maka kita bisa tambahkan di header key baru bernama Idempotency-Key yang valuenya akan digenerate di sisi frontend. Ini dilakukan agar ketika di sisi frontend terjadi timeout, maka ketika melakukan retry lagi di sisi frontend harus mengirim Idempotency-Key dengan value yang sama. Agar nanti di sisi backend akan terlihat apakah request yang dikirim ini sudah diproses atau belum. Untuk lebih jelasnya, mari saya coba gambarkan bagaimana cara idempotency ini bekerja :
Dengan menerapkan cara ini sekarang ketika melakukan proses checkout dengan data yang sama, maka data yang dibuat di backend jadi hanya 1. Dan berikut ini adalah contoh implementasi idempotency di bahasa pemrograman ruby (sinatra) :
- Frontend Request :
curl -X POST http://localhost:4567/payments \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000" \
-d '{"from": "akun_A", "to": "akun_B", "amount": 5000}'2. Backend Code :
- Gemfile
# frozen_string_literal: true
source "https://rubygems.org"
gem 'sinatra'
gem 'redis'- idempotency.rb
require 'sinatra'
require 'json'
require 'redis'
require 'securerandom'
# Inisialisasi Redis
redis = Redis.new
# Simulasi endpoint pembayaran
post '/payments' do
content_type :json
idempotency_key = request.env["HTTP_IDEMPOTENCY_KEY"]
halt 400, { error: "Idempotency-Key header is required" }.to_json unless idempotency_key
# Cek apakah sudah ada response sebelumnya
cached_response = redis.get("idempotency:#{idempotency_key}")
if cached_response
halt 400, { error: "This request has been processed." }.to_json
end
# Simulasi data request
request_payload = JSON.parse(request.body.read)
from = request_payload["from"]
to = request_payload["to"]
amount = request_payload["amount"]
# Simulasi pemrosesan transaksi
tx_id = SecureRandom.uuid
result = {
status: "success",
tx_id: tx_id,
from: from,
to: to,
amount: amount
}
# Simpan respons ke Redis dengan TTL 24 jam (86400 detik)
redis.setex("idempotency:#{idempotency_key}", 86400, result.to_json)
status 200
result.to_json
end- Setup
bundle install- Run Sinatra App
ruby idempotency.rbKalo misal ada kebutuhan untuk handle masalah race condition, maka bisa gunakan opsi nx (insert data hanya berhasil jika data tidak ada / sudah expire di redis) ketika set data ke redis. Berikut contoh codenya :
- idempotency_lock.rb
require 'sinatra'
require 'json'
require 'redis'
require 'securerandom'
# Inisialisasi Redis
redis = Redis.new
# Simulasi endpoint pembayaran
post '/payments' do
content_type :json
idempotency_key = request.env["HTTP_IDEMPOTENCY_KEY"]
halt 400, { error: "Idempotency-Key header is required" }.to_json unless idempotency_key
request_payload = JSON.parse(request.body.read)
# Simpan respons ke Redis dengan TTL 24 jam (86400 detik) dengan opsi nx
cached_response = redis.set("idempotency:#{idempotency_key}", request_payload.to_json, nx: true, ex: 86400)
# Cek apakah sudah ada response sebelumnya
if !cached_response
halt 400, { error: "This request has been processed." }.to_json
end
# Simulasi data request
from = request_payload["from"]
to = request_payload["to"]
amount = request_payload["amount"]
# Simulasi pemrosesan transaksi
tx_id = SecureRandom.uuid
result = {
status: "success",
tx_id: tx_id,
from: from,
to: to,
amount: amount
}
status 200
result.to_json
end- Run Sinatra App :
ruby idempotency_lock.rbJika teman-teman ingin melihat contoh code nya secara full, di bawah ini merupakan implementasi idempotency yang saya coba buat dengan bahasa ruby : one_take.
Nah teman-teman tadi itu merupakan penjelasan mengenai idempotency, mudah-mudahan bisa bermanfaat. Jika ada hal yang salah dari apa yang saya sampaikan, silahkan sampaikan di kolom komentar agar kita sama-sama belajar. Segitu saja dulu teman-teman tulisan saya kali ini, enjoy!









