Mengenal Idempotency Key: Rahasia Anti-Double Charge di API Pembayaran

Pernah internet kalian ngadat pas lagi nekan tombol Bayar, terus panik ngekliknya tiga kali? Kalau endpoint-nya nggak idempotent, saldo bisa kepotong berkali-kali. Yuk, kenalan sama Idempotency Key!

Mengenal Idempotency Key: Rahasia Anti-Double Charge di API Pembayaran

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/orders tiga 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:

  1. Cek kunci di Redis dulu. Backend ngecek apakah key idempotency:a1b2-c3d4-e5f6 udah pernah ada.
  2. 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.
  3. 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.
  4. 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:

PYTHON
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:

  1. 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.
  2. 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.
  3. 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! 👋

Inva

Writer

Biar makin jago ngoding.

Yuk gabung bareng temen-temen developer lainnya buat dapet update teknologi, tips, dan tutorial santai tiap minggu.

Biar Nggak Kudet

Update teknologi, tips ngoding, dan insight santai langsung ke email kamu tiap minggu.

Santai, anti spam. Bisa berhenti langganan kapan aja.

Baca Selanjutnya

Lihat semua

© 2026 Inva.dev.