argabayu— Backend & Systems Engineer

Idempotensi itu milik basis data, bukan aplikasi

2 Juli 2026·6 min baca·#go#postgres#desain

Sistem terdistribusi tidak punya “sekali”. Yang ada adalah “setidaknya sekali” ditambah strategi menghadapi duplikasi.

Kesalahan yang umum

Mengecek keberadaan dulu di aplikasi, lalu menyisipkan:

go
// 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

sql
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.

go
// 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.

sql
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.