Idempotensi itu milik basis data, bukan aplikasi
Sistem terdistribusi tidak punya “sekali”. Yang ada adalah “setidaknya sekali” ditambah strategi menghadapi duplikasi.
Kesalahan yang umum
Mengecek keberadaan dulu di aplikasi, lalu menyisipkan:
// Berlomba — dua proses bisa lewat di EXISTS bersamaan.
exists, _ := db.Exists(ctx, id)
if !exists {
db.Insert(ctx, event) // duplikat
}Ada jeda antara Exists dan Insert, dan pada jeda itulah proses lain masuk.
Ini bukan bug langka: pada retry klien, ia terjadi hampir setiap kali.
Biarkan basis data yang menjamin
INSERT INTO events (id, kind, payload)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO NOTHING;Sekarang jaminannya ada di mesin basis data, dan tidak ada jalur kode yang bisa melangkahi.
// Tidak perlu EXISTS. Konflik bukan error — itu hasil yang diharapkan.
_, err := tx.ExecContext(ctx, insertSQL, e.ID, e.Kind, e.Payload)
if err != nil {
return err // hanya error sungguhan
}Kapan DO UPDATE
Kalau event bersifat upsert — misalnya pembaruan status — pakai DO UPDATE.
Bedanya penting: DO NOTHING mempertahankan baris pertama, DO UPDATE
memakai yang terakhir.
ON CONFLICT (id) DO UPDATE
SET payload = EXCLUDED.payload,
received_at = EXCLUDED.received_at
WHERE events.received_at < EXCLUDED.received_at;Klausa WHERE di atas menjaga agar pembaruan yang datang terlambat tidak
menimpa yang lebih baru. Ini masalah nyata saat retry memakan waktu bermenit.
Yang perlu diingat
Kalau jaminan “hanya sekali” hidupmu bergantung pada aplikasi mengecek terlebih dahulu, itu bukan jaminan — itu harapan.