Pernah nggak kalian lagi bayar tagihan di aplikasi belanja, terus koneksi internetnya ngadat pas tombol "Bayar Sekarang" ditekan? Karena layarnya cuma nampilin loading spinner tanpa kepastian, kalian panik lalu ngeklik tombolnya tiga kali lagi berturut-turut.
Beberapa detik kemudian SMS pun masuk: saldo rekening kalian terpotong empat kali untuk satu pesanan yang sama. 😱
Di dunia backend engineering, mimpi buruk finansial ini disebut Double Charge. Penyebab utamanya kegagalan jaringan: request-nya sebenernya udah nyampe ke server dan diproses, tapi respon suksesnya gagal diterima aplikasi. Aplikasi pun nge-retry, dan servernya mengeksekusi ulang.
Biar transaksi tetap aman meski jaringan putus-nyambung, sistem finansial mengandalkan mekanisme bernama Idempotency Key.
Poin Penting
- Operasi idempotent itu artinya dijalankan sekali atau berkali-kali, kondisi akhir sistemnya tetap sama dan nggak ada efek samping tambahan.
- POST secara default nggak idempotent, jadi endpoint pembayaran rawan double charge kalau request-nya di-retry.
- Header Idempotency-Key berisi UUID unik per transaksi, kerjanya kayak karcis parkir: dipakai sekali, sisanya cuma diputar ulang.
- Redis dengan SET NX EX jadi guard cepat buat ngunci request yang sedang diproses, dan response aslinya disimpan buat diputar ulang.
Apa Sih Sebenarnya Sifat Idempotent Itu?
Dalam matematika dan ilmu komputer, sebuah operasi disebut idempotent kalau menjalankannya sekali atau berkali-kali menghasilkan kondisi akhir sistem yang sama persis, tanpa efek samping tambahan.
Di protokol HTTP, sifat ini melekat di beberapa method:
- GET: idempotent. Ngambil profil user 10 kali nggak bakal ngerubah saldo atau data apa pun.
- DELETE: idempotent. Ngehapus postingan dengan ID 42 sekali atau lima kali, hasil akhirnya sama: postingannya nggak ada lagi di sistem.
- POST: nggak idempotent. Manggil
POST /api/orderstiga kali bakal bikin tiga pesanan baru dan motong saldo tiga kali.
Jadi tantangannya jelas dong: gimana caranya bikin endpoint POST berperilaku idempotent saat di-retry?
Solusinya: Header Idempotency-Key
Standar industri—lihat Stripe Idempotency API dan draf IETF Idempotency-Key Header—mewajibkan klien ngirim header unik bernama Idempotency-Key, biasanya berupa string UUIDv4 acak, setiap kali ngirim request transaksi kritis.
Biar kebayang, bayangin aja mesin parkir otomatis di gedung. Mesinnya nyetak satu karcis dengan barcode unik, misalnya TRX-9988. Pas kalian mau keluar dan nempelkan karcis itu, kalian bayar Rp10.000. Kalau iseng nempelkan karcis TRX-9988 yang sama lagi semenit kemudian, mesinnya nggak akan nagih Rp10.000 baru—dia langsung nampilin status "Tiket ini sudah lunas pada pukul 14.05. Palang pintu terbuka!".
Barcode tiket itulah analogi dari Idempotency-Key.
Alur Kerjanya di Balik Layar
Ketika request masuk ke backend membawa header Idempotency-Key: a1b2-c3d4-e5f6, backend menjalankan alur proteksi ini:
- Cek kunci di Redis dulu. Backend ngecek apakah key
idempotency:a1b2-c3d4-e5f6udah pernah ada. - Kunci baru, belum pernah ada. Backend masang atomic lock pakai
SET key "PROCESSING" NX EX 120, mengeksekusi pembayaran, lalu menyimpan status dan payload response JSON ke Redis dengan masa kedaluwarsa, misalnya 24 jam. - Kuncinya sedang diproses. Kalau ada request dengan key sama masuk saat proses pertama masih berjalan, server langsung balas
409 Conflict: "Transaksi sedang diproses, harap tunggu sejenak!". Ini yang mencegah race condition, lho. - Kuncinya sudah pernah sukses. Server nggak ngeksekusi ulang pemotongan saldo atau payment gateway. Response lama diambil dari Redis dan dikirim balik sebagai cached response replay, dengan header tambahan
Idempotent-Replayed: true.
Contoh Implementasi Sederhana di FastAPI
Berikut contoh guard sederhana di FastAPI yang memanfaatkan Redis:
import json
import redis.asyncio as aioredis
from fastapi import FastAPI, Header, HTTPException, status
from pydantic import BaseModel
app = FastAPI()
r = aioredis.from_url("redis://localhost:6379", decode_responses=True)
class PaymentRequest(BaseModel):
account_id: str
amount: float
@app.post("/api/charge")
async def process_payment(
payload: PaymentRequest,
idempotency_key: str = Header(..., description="UUID unik untuk transaksi ini")
):
cache_key = f"idempotency:{idempotency_key}"
# 1. Cek apakah transaksi ini sudah pernah selesai sebelumnya
cached_data = await r.get(cache_key)
if cached_data:
record = json.loads(cached_data)
if record.get("status") == "PROCESSING":
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Transaksi dengan kunci ini sedang diproses. Mohon tunggu."
)
# Putar ulang response lama, tanpa potong saldo lagi!
return record.get("response")
# 2. Pasang lock atomik (SET NX EX) selama 60 detik
acquired = await r.set(
cache_key,
json.dumps({"status": "PROCESSING"}),
nx=True,
ex=60
)
if not acquired:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Permintaan bersamaan terdeteksi."
)
try:
# 3. Eksekusi transaksi bisnis yang sebenarnya (simulasi potong saldo)
# call_payment_gateway(payload.account_id, payload.amount)
result = {
"status": "success",
"message": f"Berhasil memotong saldo Rp{payload.amount:,.0f}",
"transaction_id": f"TRX-{idempotency_key[:8]}"
}
# 4. Simpan response sukses ke Redis (TTL 24 jam = 86400 detik)
await r.set(
cache_key,
json.dumps({"status": "COMPLETED", "response": result}),
ex=86400
)
return result
except Exception:
# Kalau terjadi error teknis tak terduga, hapus lock biar user bisa retry
await r.delete(cache_key)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Gagal memproses pembayaran. Silakan coba lagi."
)
Tiga Hal yang Sering Kelewat
Biar implementasi Idempotency Key kalian beneran tangguh, ada tiga hal yang sering kelewat nih:
- Gabungkan key-nya dengan user ID. Pakai pola
idempotency:user_123:key_abc, bukan cuma key-nya. Kalau nggak, pengguna iseng bisa nebak-nebak key milik orang lain. - Cocokkan isi request body-nya. Hitung hash SHA-256 dari payload-nya. Kalau ada request dengan key sama tapi nominalnya beda, tolak dengan
422 Unprocessable Entity. Ini ngeblokir upaya manipulasi data. - Tentukan TTL yang masuk akal. Simpan key di Redis selama 24 sampai 72 jam. Setelah periode itu, anggap hangus.
Jangan Lupa: Redis Bukan Satu-satunya Penjaga
Poin penting yang sering kelewat—Redis itu cepat sih, tapi nggak selalu jadi sumber kebenaran. Kalau Redis kalian mati atau ke-flush, semua kunci idempotency-nya hilang, dan request retry bakal lolos jadi transaksi baru.
Buat sistem pembayaran yang beneran kritis, simpan juga jejak idempotency-nya di database transaksi kalian, dengan kolom idempotency_key yang unik. Redis dipakai buat ngecek cepat di jalur utama, database dipakai buat jaminan jangka panjang. Dua-duanya jalan bareng.
Menangani jaringan yang putus-nyambung dan pengguna yang nggak sabaran itu tanggung jawab fundamental di level arsitektur. Dengan Idempotency Key di setiap endpoint kritis, sistem pembayaran kalian bakal kebal transaksi ganda dan ramah banget terhadap auto-retry.
Selamat ngoding ria, dan semoga duit pengguna kalian nggak pernah kepotong dua kali! 👋