Ilustrasi sampul artikel: Idempotency Adalah: Konsep Penting dalam Desain API (dan Cara Implementasinya)
·APIBackendTutorial

Idempotency Adalah: Konsep Penting dalam Desain API (dan Cara Implementasinya)

Idempotency adalah sifat operasi yang aman diulang tanpa efek samping ganda. Pahami HTTP method idempoten, idempotency key, dan cara cegah double payment.

Pembuat Hyper Sheets & BelajarWeb.dev

Idempotency (idempoten) adalah sifat sebuah operasi yang, jika dijalankan berkali-kali dengan input yang sama, memberikan hasil atau efek yang sama seperti dijalankan sekali. Dalam desain API, endpoint yang idempoten aman diulang tanpa menimbulkan efek samping ganda, inilah yang membuatnya penting untuk menangani retry akibat jaringan yang tidak stabil. Kalau sebuah request “hilang” di tengah jalan lalu dikirim ulang, endpoint idempoten menjamin datamu tidak berubah dua kali.

Kedengarannya sepele sampai kamu mengalami sendiri kasus paling menyeramkannya: seorang pengguna menekan tombol “Bayar”, koneksinya putus sebelum respons sampai, ia menekan lagi, dan kartunya ter-charge dua kali. Artikel ini membahas idempotency secara berurutan dan tuntas: dari definisi dan analogi sederhana, perbedaan idempoten vs safe, idempotency di tiap HTTP method (lengkap dengan tabel), kenapa ia krusial untuk retry dan pembayaran, sampai cara implementasi Idempotency-Key dengan contoh kode Laravel dan Node yang bisa langsung kamu pakai. Kita juga bahas TTL, race condition, dan bagaimana payment gateway seperti Stripe dan Midtrans memakainya di dunia nyata.

Idempotency Adalah… (Definisi dan Analogi Sederhana)

Idempotency adalah properti sebuah operasi di mana menjalankannya sekali atau berkali-kali dengan input yang sama menghasilkan keadaan akhir yang identik. Istilah ini berasal dari matematika: sebuah fungsi f disebut idempoten kalau f(f(x)) = f(x). Fungsi nilai absolut adalah contoh klasiknya, abs(abs(-5)) sama dengan abs(-5), yaitu 5; menerapkannya dua kali tidak mengubah apa-apa dibanding menerapkannya sekali.

Analogi paling gampang: tombol lantai di dalam lift. Kamu menekan tombol “lantai 3” satu kali, lift akan ke lantai 3. Kamu menekannya lima kali karena tidak sabar? Lift tetap ke lantai 3, hasilnya sama persis. Menekan tombol itu adalah operasi idempoten. Contoh lain: tombol lampu yang men-set “nyala”. Ditekan sekali atau sepuluh kali, keadaan akhirnya sama: lampu nyala.

Sekarang bandingkan dengan operasi yang tidak idempoten: “tambah 1 ke saldo”. Kalau operasi ini dijalankan lima kali, saldomu naik 5, bukan 1. Setiap pengulangan mengubah keadaan lagi dan lagi, hasilnya bergantung pada berapa kali ia dijalankan. Ini persis kenapa operasi bergaya “increment”, “kurangi stok”, atau “tarik uang dari ATM” berbahaya untuk diulang tanpa pengaman.

Dalam bahasa API, bedanya begini:

# Idempoten, set nilai absolut
PUT /users/1   { "nama": "Budi" }   # hasil akhir: nama = "Budi"
PUT /users/1   { "nama": "Budi" }   # diulang -> nama TETAP "Budi"

# TIDAK idempoten, operasi relatif
POST /users/1/saldo/tambah   { "jumlah": 10000 }   # saldo +10.000
POST /users/1/saldo/tambah   { "jumlah": 10000 }   # diulang -> saldo +10.000 LAGI

Kata kunci yang perlu kamu pegang: idempotency berbicara tentang efek pada keadaan server, bukan tentang apakah respons yang kembali persis sama. Kita akan lihat nuansa ini lagi saat membahas DELETE. Konsep ini adalah fondasi desain API yang tahan banting, kalau kamu ingin mendalaminya dengan latihan bertahap, lesson idempotency di Hyper Sheets membahasnya langkah demi langkah dengan editor langsung di browser.

Idempoten vs Safe: Dua Sifat yang Sering Tertukar

Safe berarti operasi tidak mengubah keadaan server sama sekali (read-only), sedangkan idempoten berarti operasi boleh mengubah keadaan, tapi mengubahnya berkali-kali sama saja dengan sekali. Semua operasi yang safe otomatis idempoten, kalau kamu tidak mengubah apa pun, tentu mengulanginya juga tidak mengubah apa pun. Tapi kebalikannya tidak berlaku: ada operasi idempoten yang tidak safe.

Contoh paling jelas adalah DELETE. Menghapus sebuah resource jelas mengubah keadaan server, jadi ia tidak safe. Tapi ia idempoten: setelah resource terhapus, memanggil DELETE lagi tidak mengubah keadaan, resource itu tetap tidak ada. Hasil akhirnya identik.

Sifat Arti Mengubah data? Contoh method
Safe Read-only, tidak menyentuh state Tidak GET, HEAD, OPTIONS
Idempoten Diulang = sekali, secara efek Boleh GET, PUT, DELETE

Cara menghafalnya: safe adalah bagian “tidak mengubah apa-apa”, idempoten adalah bagian “mengubahnya aman diulang”. Setiap method yang safe pasti idempoten, tapi tidak setiap yang idempoten itu safe. Perbedaan ini bukan cuma soal istilah, ia menentukan mana endpoint yang aman di-retry otomatis oleh klien, dan mana yang butuh pengaman tambahan.

Idempotency di HTTP Method: GET, POST, PUT, DELETE, PATCH

Menurut spesifikasi HTTP, GET, HEAD, OPTIONS, PUT, dan DELETE bersifat idempoten, sedangkan POST tidak; PATCH bisa idempoten atau tidak tergantung isi operasinya. Ini bukan aturan yang dipaksakan server secara otomatis, ini kontrak yang seharusnya kamu penuhi saat mendesain endpoint, supaya klien dan proxy tahu mana request yang aman diulang. Berikut peta lengkapnya:

Method Safe? Idempoten? Keterangan
GET Ya Ya Hanya membaca data
HEAD Ya Ya Seperti GET tapi tanpa body respons
OPTIONS Ya Ya Menanyakan kemampuan endpoint
PUT Tidak Ya Mengganti resource seutuhnya, hasil akhir sama walau diulang
DELETE Tidak Ya Menghapus resource, hapus lagi, tetap “tidak ada”
POST Tidak Tidak Membuat resource baru setiap kali dipanggil
PATCH Tidak Tergantung Idempoten jika operasinya absolut, tidak jika relatif

Mari bedah yang paling sering bikin bingung.

PUT itu idempoten karena sifatnya “mengganti seutuhnya”. PUT /users/1 dengan body { "nama": "Budi", "email": "[email protected]" } menetapkan seluruh resource ke keadaan itu. Kirim sekali atau sepuluh kali, hasil akhirnya sama: user 1 bernama Budi. Ini berbeda dari sekadar “menambah”.

DELETE idempoten dari sisi keadaan, meski respons bisa berbeda. DELETE /users/1 pertama menghapus user dan mungkin membalas 204 No Content. Panggilan kedua menemukan user sudah tidak ada dan mungkin membalas 404 Not Found. Kode statusnya beda, tapi keadaan server identik: user 1 tetap tidak ada. Sekali lagi, idempotency menilai efek pada data, bukan kesamaan respons.

POST tidak idempoten karena sifatnya “membuat yang baru”. Setiap POST /orders menciptakan order baru dengan ID baru. Panggil tiga kali, kamu dapat tiga order. Inilah method yang paling rawan menimbulkan duplikat saat di-retry, dan yang paling butuh Idempotency-Key, seperti kita bahas nanti.

PATCH tergantung isi operasinya. PATCH yang menetapkan nilai absolut bersifat idempoten; yang melakukan operasi relatif tidak:

# PATCH idempoten, set nilai absolut
PATCH /produk/1   { "stok": 100 }        # stok jadi 100
PATCH /produk/1   { "stok": 100 }        # diulang -> stok TETAP 100

# PATCH TIDAK idempoten, operasi relatif
PATCH /produk/1   { "stok_berkurang": 1 } # stok -1
PATCH /produk/1   { "stok_berkurang": 1 } # diulang -> stok -1 LAGI

Memahami sifat tiap method ini adalah bagian inti dari mendesain API yang benar. Kalau kamu ingin melihat bagaimana pemilihan method yang tepat memengaruhi keseluruhan arsitektur endpoint, lesson HTTP method dan lesson desain API di Hyper Sheets membahas keduanya dengan contoh praktik yang bisa langsung kamu coba.

Kenapa Idempotency Penting: Retry, Timeout, dan Double Charge

Idempotency penting karena jaringan itu tidak bisa dipercaya: request bisa hilang, respons bisa lambat, dan klien akan mengirim ulang, tanpa endpoint idempoten, setiap retry berpotensi menggandakan efeknya. Di dunia distributed system, ada aksioma yang tidak bisa dihindari: kamu tidak pernah benar-benar tahu apakah sebuah request gagal karena tidak sampai, atau berhasil tapi respons suksesnya yang hilang di jalan. Dari sisi klien, keduanya terlihat identik, sama-sama timeout.

Perhatikan skenario timeout ini baik-baik:

Klien  --- POST /payments ---> Server   (request SAMPAI)
Klien  <--- X  (timeout)  ---  Server   (server sudah CHARGE,
                                          tapi respons hilang)
Klien  --- POST /payments ---> Server   (klien retry, mengira gagal)
                                          -> CHARGE KEDUA KALINYA

Server sudah memproses pembayaran pertama dengan benar. Tapi karena respons 200 OK-nya tidak sampai ke klien (jaringan putus, timeout, load balancer), klien menyimpulkan “gagal” dan mengirim ulang. Tanpa pengaman idempotency, server memperlakukan request kedua sebagai pembayaran baru, dan pelanggan ter-charge dua kali. Ini bukan skenario teoretis; ini kelas bug yang benar-benar terjadi di production sistem pembayaran.

Ada tiga sumber pengulangan yang membuat idempotency wajib:

  • Retry otomatis oleh klien atau library. Banyak HTTP client dan message queue melakukan retry otomatis saat timeout. Ini fitur bagus untuk keandalan, tapi berbahaya kalau endpoint-nya tidak idempoten.
  • Network timeout. Seperti skenario di atas, respons sukses yang hilang membuat klien mengira operasi gagal padahal sudah berhasil.
  • At-least-once delivery. Kebanyakan sistem antrian pesan (message queue) menjamin sebuah pesan dikirim minimal sekali, artinya kadang lebih dari sekali. Consumer yang memproses pesan wajib idempoten, atau efeknya berlipat.

Kasus paling nyata dan paling relevan buat kita di Indonesia adalah pembayaran. Bayangkan alur checkout: pengguna menekan tombol “Bayar”, sinyal HP-nya jelek, halaman loading lama, ia panik dan menekan lagi. Atau ia me-refresh halaman yang menampilkan form pembayaran. Tanpa idempotency, setiap klik/refresh bisa jadi satu transaksi. Idempotency-lah yang membuat “klik dua kali” tetap menghasilkan satu pembayaran. Selain pembayaran, pola ini juga menyelamatkan operasi seperti “kirim email konfirmasi”, “kurangi stok”, dan “transfer saldo” dari efek ganda. Untuk memahami bagaimana kegagalan seperti ini seharusnya dikomunikasikan ke klien lewat respons yang benar, lesson error handling API melengkapi topik ini dengan pola penanganan error yang rapi.

Cara Implementasi Idempotency Key

Cara paling umum membuat endpoint POST menjadi aman diulang adalah dengan idempotency key: klien mengirim header Idempotency-Key berisi nilai unik (biasanya UUID), server menyimpan pasangan key dan hasilnya, lalu jika request dengan key yang sama datang lagi, server mengembalikan hasil yang tersimpan alih-alih memprosesnya ulang. Dengan begini, method yang secara alami tidak idempoten seperti POST dibuat idempoten secara buatan, dan efek gandanya dicegah di sisi server.

Kuncinya ada satu aturan yang sering dilanggar: key dibuat sekali per operasi, lalu dipakai ulang untuk semua retry operasi itu. Kalau klien membuat key baru di setiap retry, seluruh mekanisme ini runtuh, server akan menganggapnya operasi yang berbeda. Berikut cara klien membuat dan mengirimnya:

// Klien: buat SATU key per operasi, simpan, pakai ulang untuk retry
const key = crypto.randomUUID(); // contoh: "3f9a1c2e-7b0d-4a1e-9c33-..."

async function bayar() {
  return fetch('/api/v1/payments', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Idempotency-Key': key, // SAMA untuk setiap percobaan operasi ini
    },
    body: JSON.stringify({ amount: 150000, order_id: 'ORD-123' }),
  });
}

// Saat timeout, panggil bayar() lagi TANPA membuat key baru

Alur di sisi server dalam pseudocode:

fungsi tanganiRequest(request):
    key = request.header("Idempotency-Key")

    kalau key kosong:
        return proses(request)      # tidak idempoten, terserah pemanggil

    tersimpan = store.ambil(key)
    kalau tersimpan ada:
        return tersimpan.response   # kembalikan hasil lama, JANGAN proses ulang

    response = proses(request)      # proses TEPAT sekali
    store.simpan(key, response)     # simpan hasilnya untuk request berikutnya
    return response

Tabel idempotency_keys

Server butuh tempat menyimpan key beserta hasilnya. Skema tabel minimalnya seperti ini (contoh migration Laravel):

Schema::create('idempotency_keys', function (Blueprint $table) {
    $table->string('id')->primary();   // nilai Idempotency-Key (UUID)
    $table->string('status');          // "processing" atau "completed"
    $table->unsignedSmallInteger('response_status')->nullable();
    $table->longText('response_body')->nullable();
    $table->timestamp('created_at');
    $table->index('created_at');       // untuk pembersihan berdasarkan TTL
});

Yang wajib disimpan bukan cuma “key ini sudah pernah dilihat”, tapi juga status kode dan body respons aslinya, supaya retry menerima jawaban yang sama persis dengan request pertama, bukan sekadar pesan “sudah diproses”. Menjadikan id sebagai primary key juga sekaligus memasang unique constraint yang nanti menyelamatkan kita dari race condition.

Contoh Middleware Laravel

Di Laravel, cara paling rapi adalah membungkus logika ini dalam middleware yang dipasang pada route yang butuh idempotency:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class Idempotency
{
    public function handle(Request $request, Closure $next)
    {
        // Hanya berlaku untuk method yang mengubah data
        if (! in_array($request->method(), ['POST', 'PATCH'])) {
            return $next($request);
        }

        $key = $request->header('Idempotency-Key');
        if (! $key) {
            return $next($request); // tanpa key -> jalan seperti biasa
        }

        // 1. Sudah pernah selesai? Kembalikan hasil tersimpan.
        $saved = DB::table('idempotency_keys')->where('id', $key)->first();
        if ($saved && $saved->status === 'completed') {
            return response($saved->response_body, $saved->response_status)
                ->header('Content-Type', 'application/json')
                ->header('Idempotent-Replayed', 'true');
        }

        // 2. Kunci key ini lebih dulu; unique constraint mencegah dua
        //    request paralel sama-sama masuk.
        try {
            DB::table('idempotency_keys')->insert([
                'id'         => $key,
                'status'     => 'processing',
                'created_at' => now(),
            ]);
        } catch (\Illuminate\Database\QueryException $e) {
            // Request paralel lain sudah menyisipkan key yang sama
            return response()->json(
                ['message' => 'Request dengan Idempotency-Key ini sedang diproses'],
                409 // Conflict
            );
        }

        // 3. Proses request TEPAT sekali, lalu simpan hasilnya.
        $response = $next($request);

        DB::table('idempotency_keys')->where('id', $key)->update([
            'status'          => 'completed',
            'response_status' => $response->getStatusCode(),
            'response_body'   => $response->getContent(),
        ]);

        return $response;
    }
}

Perhatikan tiga langkah bernomor di kode: (1) cek hasil tersimpan dan kembalikan kalau ada, (2) “klaim” key dengan insert yang dilindungi unique constraint, (3) proses lalu simpan. Langkah 2 itu jantung anti-race-condition-nya, kita bahas sebentar lagi. Kalau kamu baru mengenal middleware dan alur request di Laravel, panduan belajar Laravel untuk pemula menjelaskan konsep middleware, routing, dan controller dari nol.

Contoh Middleware Node/Express

Pola yang sama di Node.js dengan Express. Di sini kita pakai Map untuk contoh, tapi di produksi gantilah dengan Redis atau database supaya key bertahan antar-proses:

// idempotency.js, middleware Express sederhana
const store = new Map(); // GANTI dengan Redis/DB di produksi

function idempotency(req, res, next) {
  if (!['POST', 'PATCH'].includes(req.method)) return next();

  const key = req.get('Idempotency-Key');
  if (!key) return next();

  const saved = store.get(key);
  if (saved) {
    res.set('Idempotent-Replayed', 'true');
    return res.status(saved.status).json(saved.body);
  }

  // Bungkus res.json supaya hasilnya ikut tersimpan
  const originalJson = res.json.bind(res);
  res.json = (body) => {
    store.set(key, { status: res.statusCode, body });
    return originalJson(body);
  };

  next();
}

module.exports = idempotency;

Logikanya identik dengan versi Laravel: cek key, kembalikan hasil tersimpan kalau ada, jika belum ada proses seperti biasa sambil menyimpan respons untuk request berikutnya. Kalau JavaScript dan cara kerja fungsi seperti res.json di atas masih terasa asing, dasar-dasar JavaScript bisa jadi titik mulai yang bagus sebelum masuk ke middleware.

Untuk membuktikan mekanismenya bekerja, coba kirim dua request identik dengan curl:

# Request pertama: diproses, pembayaran di-charge sekali
curl -X POST https://api.tokoku.dev/v1/payments \
  -H "Idempotency-Key: 3f9a1c2e-7b0d-4a1e-9c33-a1b2c3d4e5f6" \
  -H "Content-Type: application/json" \
  -d '{"amount": 150000, "order_id": "ORD-123"}'

# Timeout? Kirim ULANG dengan key yang SAMA.
# Server mengembalikan hasil yang sama, TIDAK charge dua kali.
curl -X POST https://api.tokoku.dev/v1/payments \
  -H "Idempotency-Key: 3f9a1c2e-7b0d-4a1e-9c33-a1b2c3d4e5f6" \
  -H "Content-Type: application/json" \
  -d '{"amount": 150000, "order_id": "ORD-123"}'

Request kedua akan membawa header Idempotent-Replayed: true dan body yang identik dengan yang pertama, bukti bahwa server hanya benar-benar memproses sekali.

TTL dan Pembersihan Idempotency Key

Idempotency key tidak perlu disimpan selamanya, beri masa berlaku (TTL) seperti 24 jam, karena retry yang wajar terjadi dalam hitungan detik sampai jam, bukan minggu. Kalau key dibiarkan menumpuk tanpa dibersihkan, tabel idempotency_keys akan tumbuh tanpa batas dan lama-lama membebani database. TTL menjaga tabel tetap ramping sekaligus tetap melindungi dari retry dalam jendela waktu yang realistis.

Kalau kamu menyimpan key di Redis, TTL nyaris gratis, cukup set masa kedaluwarsa saat menulis:

SET idem:3f9a1c2e-... "<hasil>" EX 86400   # kedaluwarsa 24 jam (86400 detik)

Kalau kamu memakai tabel database, jalankan pembersihan terjadwal. Di Laravel kamu bisa memakai scheduler untuk menghapus key lama secara berkala:

// routes/console.php, hapus key yang lebih tua dari 24 jam
Schedule::call(function () {
    DB::table('idempotency_keys')
        ->where('created_at', '<', now()->subDay())
        ->delete();
})->hourly();

Atau langsung sebagai query SQL yang bisa dijadwalkan lewat cron:

DELETE FROM idempotency_keys
WHERE created_at < NOW() - INTERVAL 24 HOUR;

Berapa lama TTL idealnya? Sesuaikan dengan pola retry sistemmu. Untuk request interaktif dari pengguna, beberapa jam sudah cukup. Untuk pesan dari message queue yang mungkin di-retry berjam-jam kemudian, perpanjang jadi 24 jam atau lebih. Yang penting: cukup panjang untuk menangkap semua retry yang masuk akal, cukup pendek untuk tidak membebani penyimpanan. Sebagai pembanding, payment gateway besar umumnya menyimpan idempotency key selama sekitar 24 jam.

Idempotency vs Deduplication vs Exactly-Once

Idempotency membuat pemrosesan berulang aman di sisi penerima, deduplication membuang pesan duplikat sebelum diproses, dan exactly-once adalah jaminan ideal bahwa sebuah operasi dieksekusi tepat sekali, yang dalam praktik dicapai dengan menggabungkan at-least-once delivery plus pemrosesan idempoten. Ketiganya sering tercampur, padahal perannya berbeda dan saling melengkapi.

Bedah satu per satu:

  • Deduplication bekerja di sisi pengiriman/masuk: sistem mendeteksi bahwa sebuah pesan atau request adalah duplikat (lewat ID unik) dan membuangnya sebelum sampai ke logika bisnis. Idempotency key sebenarnya salah satu bentuk deduplication di level aplikasi.
  • Idempotency bekerja di sisi pemrosesan: bahkan kalau pesan yang sama diproses dua kali, efeknya tetap seperti sekali. Ia tidak berusaha mencegah duplikat, tapi membuat duplikat jadi tidak berbahaya.
  • Exactly-once adalah tujuan akhir: setiap operasi berdampak tepat sekali. Banyak orang mengira ini bisa dijamin murni oleh sistem antrian, padahal secara teori pengiriman exactly-once yang sejati itu sangat sulit di jaringan yang tidak andal. Yang bisa dicapai di dunia nyata adalah at-least-once delivery + idempotent processing, pesan boleh datang lebih dari sekali, tapi karena diproses secara idempoten, hasil akhirnya seolah tepat sekali.

Intinya: idempotency adalah cara praktis mendekati exactly-once tanpa harus memecahkan masalah yang secara teori nyaris mustahil. Kamu tidak melawan duplikasi, kamu membuatnya tidak berdampak.

Idempotency di Webhook dan Payment Gateway

Webhook dan payment gateway adalah dua tempat di mana idempotency paling terasa wajib: keduanya melakukan retry secara agresif, jadi penerima harus siap menerima event yang sama lebih dari sekali. Kalau kamu pernah mengintegrasikan pembayaran atau menerima notifikasi otomatis dari layanan pihak ketiga, kamu sudah berhadapan dengan idempotency, entah sadar atau tidak.

Webhook adalah panggilan HTTP yang dikirim server lain ke endpoint-mu saat suatu event terjadi (misalnya “pembayaran berhasil”). Pengirim webhook biasanya menerapkan kebijakan retry sampai berhasil: kalau endpoint-mu membalas lambat atau error, event yang sama dikirim ulang beberapa kali. Artinya endpoint penerima webhook wajib idempoten, biasanya dengan menyimpan event_id dari setiap event yang sudah diproses, lalu mengabaikan event dengan ID yang sudah pernah dilihat:

// Handler webhook: abaikan event yang sudah pernah diproses
public function handle(Request $request)
{
    $eventId = $request->input('event_id');

    if (DB::table('processed_events')->where('id', $eventId)->exists()) {
        return response()->noContent(); // sudah diproses, cukup balas 2xx
    }

    // ... proses event (update order, kirim email, dll) ...

    DB::table('processed_events')->insert([
        'id' => $eventId,
        'processed_at' => now(),
    ]);

    return response()->noContent();
}

Payment gateway menjadikan idempotency sebagai fitur kelas satu. Stripe, misalnya, menyediakan header Idempotency-Key persis seperti yang kita bangun di atas, kamu mengirim key unik saat membuat charge, dan kalau request itu di-retry dengan key yang sama, Stripe mengembalikan hasil charge yang pertama alih-alih menagih ulang. Midtrans, gateway yang populer di Indonesia, mengandalkan order_id yang unik per transaksi untuk tujuan serupa: sebuah order_id hanya bisa dipakai untuk satu transaksi, sehingga percobaan membuat transaksi dengan order_id yang sama tidak menghasilkan pembayaran ganda. Polanya sama, namanya saja berbeda.

Pelajaran praktisnya: saat kamu jadi klien payment gateway, selalu kirim idempotency key (atau order_id unik) dan pakai ulang key itu untuk semua retry transaksi yang sama. Dan saat kamu jadi penerima webhook, selalu simpan ID event yang sudah diproses. Idempotency di sini sering berjalan berdampingan dengan mekanisme perlindungan lain seperti rate limiting untuk membatasi laju request, keduanya bagian dari membangun endpoint yang tangguh.

Kesalahan Umum Saat Menerapkan Idempotency

Kesalahan paling sering seputar idempotency: mengira semua GET otomatis aman, memakai POST untuk pembayaran tanpa idempotency key, membuat key yang tidak unik, hanya menandai key tanpa menyimpan hasil, dan mengabaikan race condition antar dua request paralel. Berikut daftar lengkapnya beserta solusinya, sebagian besar bug idempotency yang akan kamu temui ada di sini.

1. Mengira semua GET otomatis aman padahal ada side-effect tersembunyi

GET seharusnya safe, tapi spesifikasi tidak memaksanya. Kalau kamu diam-diam menaruh efek samping di endpoint GET, misalnya GET /kirim-ulang-otp yang mengirim SMS, atau GET /produk/1 yang menambah penghitung view di setiap panggilan, kamu melanggar kontrak. Prefetch browser, crawler, atau retry proxy bisa memanggil GET kapan saja tanpa niat pengguna, dan side-effect-mu ikut terpicu. Aturannya tegas: jangan pernah menaruh operasi yang mengubah data penting di balik GET. Kalau ada efek samping, pakai POST/PUT/DELETE sesuai maksudnya.

2. POST pembayaran tanpa idempotency key

Ini kesalahan yang paling mahal. Endpoint POST /payments tanpa idempotency key adalah undangan terbuka untuk double charge begitu ada timeout atau klik ganda. Setiap endpoint yang membuat transaksi keuangan, mengurangi stok, atau memicu aksi yang tidak bisa dibatalkan wajib dilindungi idempotency key. Jangan mengandalkan asumsi “pengguna tidak akan klik dua kali”, mereka pasti akan, dan jaringan akan melakukannya untuk mereka.

3. Key tidak unik per operasi

Idempotency key harus unik untuk setiap operasi berbeda, tapi sama untuk retry dari operasi yang sama. Dua kesalahan cermin di sini: (a) memakai satu key statis untuk semua request, request kedua yang beda maksud malah dianggap duplikat dan ditolak; (b) membuat key baru di setiap retry, server menganggapnya operasi baru dan efek ganda tetap terjadi. Solusi yang benar: buat UUID sekali saat operasi dimulai (misalnya saat form checkout dibuka), simpan, dan pakai ulang untuk setiap percobaan operasi itu.

4. Hanya menandai “sudah diproses” tanpa menyimpan hasil

Kalau server hanya mencatat “key ini sudah dilihat” lalu membalas retry dengan pesan generik seperti “duplikat, ditolak”, klien tidak mendapat hasil operasi yang sebenarnya, padahal operasinya berhasil. Klien bisa jadi menampilkan error ke pengguna padahal pembayaran sukses. Simpanlah status dan body respons asli, lalu kembalikan respons yang sama persis saat retry datang. Retry harus tidak bisa dibedakan dari melihat hasil request pertama.

5. Race condition: dua request paralel dengan key yang sama

Ini kesalahan paling halus. Bayangkan dua request dengan key identik tiba nyaris bersamaan. Keduanya mengecek tabel, sama-sama melihat “key belum ada”, lalu sama-sama lanjut memproses, double charge tetap terjadi meski idempotency key ada. Cek-lalu-tulis yang tidak atomik bocor di sini. Solusinya: andalkan unique constraint atau lock di level database. Di contoh Laravel kita, id adalah primary key, jadi request kedua yang mencoba insert key yang sama akan gagal dengan QueryException, dan kita tangani dengan membalas 409 Conflict, bukan memproses ulang.

// Unique constraint = penjaga race condition
try {
    DB::table('idempotency_keys')->insert(['id' => $key, /* ... */]);
} catch (\Illuminate\Database\QueryException $e) {
    // Request paralel sudah mengklaim key ini lebih dulu
    return response()->json(['message' => 'Sedang diproses'], 409);
}

6. Tidak mengikat key ke isi request

Idempotency key idealnya juga menjaga agar isi request tidak diam-diam berubah. Kalau klien mengirim key yang sama tapi dengan body berbeda (misalnya jumlah pembayaran berubah dari 150.000 jadi 500.000), itu tanda bug atau penyalahgunaan. Praktik yang lebih aman: simpan juga sidik jari (hash) dari body request bersama key-nya, dan tolak dengan error kalau key yang sama datang membawa payload yang berbeda. Ini mencegah key dipakai ulang untuk operasi yang bukan pengulangan sejati. Menerapkan konsistensi seperti ini rapi dilakukan sebagai bagian dari desain endpoint CRUD yang matang, lesson CRUD API membahas bagaimana operasi create/update/delete sebaiknya distrukturkan.

Cek Pemahaman

HTTP method mana yang TIDAK idempoten?

FAQ Seputar Idempotency

Idempotency adalah apa?

Idempotency (idempoten) adalah sifat sebuah operasi yang, jika dijalankan berkali-kali dengan input yang sama, memberikan hasil atau efek akhir yang sama seperti dijalankan sekali. Istilahnya berasal dari matematika (f(f(x)) = f(x)). Dalam desain API, endpoint idempoten aman diulang tanpa menimbulkan efek samping ganda, properti yang krusial untuk menangani retry akibat jaringan yang tidak stabil.

Apa itu idempotency key?

Idempotency key adalah nilai unik (umumnya UUID) yang dikirim klien lewat header Idempotency-Key untuk menandai sebuah operasi. Server menyimpan pasangan key dan hasilnya; jika request dengan key yang sama datang lagi, server mengembalikan hasil yang tersimpan alih-alih memprosesnya ulang. Key ini dibuat sekali per operasi dan dipakai ulang untuk semua retry operasi tersebut, sehingga method yang tidak idempoten seperti POST bisa dibuat aman diulang.

HTTP method apa saja yang idempoten?

Menurut spesifikasi HTTP, GET, HEAD, OPTIONS, PUT, dan DELETE bersifat idempoten. POST tidak idempoten. PATCH bisa idempoten atau tidak, tergantung isinya, idempoten kalau menetapkan nilai absolut (stok = 100), tidak idempoten kalau operasinya relatif (stok = stok - 1). Perlu diingat, ini kontrak desain yang harus kamu penuhi, bukan sesuatu yang otomatis dipaksakan server.

Kenapa POST tidak idempoten?

Karena POST secara semantik berarti “membuat resource baru” setiap kali dipanggil. Panggil POST /orders tiga kali, kamu mendapat tiga order berbeda dengan tiga ID berbeda. Setiap pengulangan menambah keadaan baru, bukan menghasilkan keadaan yang sama. Inilah kenapa POST adalah method yang paling rawan menimbulkan duplikat saat di-retry, dan paling membutuhkan perlindungan idempotency key.

Bagaimana cara mencegah double payment?

Lindungi endpoint pembayaran dengan idempotency key: klien membuat satu UUID per transaksi dan mengirimnya di header Idempotency-Key, lalu memakai key yang sama untuk setiap retry. Server menyimpan key beserta hasil transaksinya; kalau key yang sama datang lagi (karena timeout atau klik ganda), server mengembalikan hasil yang tersimpan tanpa menagih ulang. Gunakan unique constraint di database untuk mencegah race condition antar request paralel, dan simpan status serta body respons asli, bukan sekadar menandai “sudah diproses”. Payment gateway seperti Stripe dan Midtrans menyediakan mekanisme ini bawaan.

Apa beda idempotency dan safe method?

Safe method adalah operasi yang tidak mengubah keadaan server sama sekali (read-only), seperti GET dan HEAD. Idempoten adalah operasi yang boleh mengubah keadaan, tapi mengubahnya berkali-kali sama saja dengan sekali, seperti PUT dan DELETE. Setiap method yang safe pasti idempoten, tapi tidak setiap method idempoten itu safe, DELETE idempoten (menghapus dua kali sama saja resource tetap tidak ada) tapi tidak safe (ia mengubah data).

Langkah Selanjutnya

Kamu sekarang sudah pegang seluruh peta idempotency: definisi dan analoginya, perbedaan idempoten vs safe, sifat tiap HTTP method, alasan ia wajib untuk retry dan pembayaran, cara implementasi Idempotency-Key lengkap dengan kode Laravel dan Node, penanganan TTL, sampai race condition dan integrasi webhook/payment gateway. Yang tersisa tinggal mempraktikkannya di endpoint nyata, dan idempotency termasuk konsep yang benar-benar melekat begitu kamu pernah sekali menyelamatkan sistem dari double charge dengannya.

Mulai dari lesson idempotency di Hyper Sheets yang latihannya langsung dinilai di browser, lalu perkuat fondasinya dengan lesson desain API supaya keputusan seperti “method apa untuk endpoint ini” jadi refleks. Dan kalau kamu membangun backend-nya di Laravel, terapkan middleware idempotency yang kita tulis tadi di proyek nyata, konteks Laravel dari awal bisa kamu ikuti lewat panduan belajar Laravel untuk pemula. Selamat ngoding!