Koordinasikan pekerjaan bertahap
Kenalkan outbox, rekonsiliasi, kompensasi, dan alasan dual-write lintas layanan bisa gagal.
Pertanyaan
Bagaimana menjaga beberapa sistem tetap sejalan ketika satu transaksi nggak bisa mencakup semuanya?
Setelah baca ini, kamu bisa menjelaskan kenapa menulis ke database dan ke layanan lain secara berurutan bisa meninggalkan keadaan setengah jadi, dan memilih antara outbox, kompensasi, dan rekonsiliasi.
Prasyarat dan contoh
Kamu paham transaksi dan atomicity (M05.01), serta reservasi dan transisi status (M05.05).
Checkout: setelah pembeli membayar, layanan pesanan harus (1) menyimpan pesanan DIBAYAR di
databasenya dan (2) mengirim event ke layanan pengiriman supaya paket disiapkan. Kodenya:
BEGIN; UPDATE pesanan SET status = 'DIBAYAR' ...; COMMIT;
kirim_ke_broker("pesanan_dibayar", id);
Kalau proses mati antara COMMIT dan kirim_ke_broker, pesanan sudah dibayar tapi paket nggak
pernah disiapkan. Kalau urutannya dibalik (kirim dulu), event bisa terkirim sementara COMMIT
gagal: paket disiapkan untuk pesanan yang nggak ada.
Mekanisme
Dual-write. Menulis ke dua sistem terpisah tanpa transaksi bersama. Salah satu bisa berhasil dan yang lain gagal, dan penyebabnya banyak: crash, timeout (hasil nggak diketahui), jaringan putus. Nggak ada urutan dua penulisan yang aman dari semua kegagalan.
Transactional outbox. Tulis event ke tabel outbox di transaksi yang sama dengan perubahan
bisnis.
BEGIN;
UPDATE pesanan SET status = 'DIBAYAR' WHERE id = :id;
INSERT INTO outbox (id, topik, payload, dikirim) VALUES (:eid, 'pesanan_dibayar', :p, false);
COMMIT;
Proses terpisah (relay) membaca baris outbox yang belum terkirim, mengirim ke broker, lalu
menandainya terkirim. Atomicity database menjamin: pesanan berubah jika dan hanya jika event
tercatat. Alternatifnya, change data capture membaca log database dan memicu pengiriman tanpa
polling.
At-least-once, maka konsumen harus idempoten. Relay bisa mengirim, lalu crash sebelum menandai
terkirim. Saat dijalankan ulang, event terkirim dua kali. Konsumen menyimpan eid yang sudah
diproses (UNIQUE) dan mengabaikan duplikat. Outbox memberi "tidak hilang", bukan "tepat sekali".
Detailnya di M10.
Kompensasi. Beberapa langkah lintas layanan, tiap langkah punya transaksi lokal sendiri. Kalau langkah ke-3 gagal, langkah 1 dan 2 dibatalkan dengan aksi balik, bukan rollback:
tahan stok --> tagih kartu --> buat pengiriman (gagal)
kompensasi: refund --> lepas stok
Kompensasi harus idempoten dan bisa dicoba ulang. Nggak semua langkah bisa dibalik: email yang sudah terkirim nggak bisa ditarik, jadi urutkan langkah yang nggak bisa dibalik di akhir.
Rekonsiliasi. Pekerjaan berkala yang membandingkan dua sumber dan memperbaiki selisih.
Contoh: tiap 5 menit, cari pesanan DIBAYAR lebih dari 10 menit yang belum punya pengiriman, lalu
terbitkan ulang event atau tandai untuk ditinjau. Rekonsiliasi adalah jaring pengaman untuk
kegagalan yang tidak terpikirkan, dan sumber kebenaran harus ditentukan (biasanya layanan pemilik
data).
Status yang jujur. Saat provider pembayaran time out, hasilnya nggak diketahui. Simpan status
MENUNGGU_KONFIRMASI dan tanya ulang atau tunggu webhook, bukan menebak GAGAL. Menagih ulang
tanpa kunci idempoten memotong saldo dua kali.
Saat rancangan ini gagal
- Menunggu relay yang mati. Tabel outbox menumpuk dan event terlambat berjam-jam. Pantau umur baris tertua yang belum terkirim, bukan hanya ukuran tabelnya.
- Konsumen tidak idempoten. Duplikat membuat dua pengiriman atau dua email.
- Kompensasi yang gagal. Refund pun bisa gagal. Perlu retry, batas percobaan, dan antrean tinjau manual.
- Mengira outbox menjamin urutan. Event untuk pesanan yang sama bisa tiba terbalik bila relay paralel. Sertakan nomor versi atau urutkan per kunci.
- Rekonsiliasi tanpa pemilik data. Dua sistem saling "memperbaiki" dan saling menimpa. Tetapkan satu sumber kebenaran.
- Transaksi terdistribusi (two-phase commit) sebagai jalan pintas. Memblokir saat koordinator mati dan jarang didukung lintas penyedia. Biasanya bukan pilihan pertama.
Latihan
Layanan pesanan menyimpan pesanan lalu memanggil HTTP POST /kirim ke layanan pengiriman.
Pada hari tertentu 0,3% panggilan time out setelah 5 detik. Dari 100.000 pesanan/hari, itu 300
pesanan. Kadang pengiriman sebenarnya sudah menerima permintaan.
Rancang perbaikan dan jelaskan kenapa tidak cukup dengan "retry saja".
Lihat pembahasan
"Retry saja" bermasalah: pada time out kita tidak tahu apakah pengiriman sudah memprosesnya.
Retry tanpa idempotensi bisa membuat dua pengiriman untuk satu pesanan.
Rancangan:
- Outbox. Saat pesanan
DIBAYAR, tulis barisoutbox(eid,pesanan_id) di transaksi yang sama. Pesanan dan niat kirim tidak bisa terpisah, walau layanan crash. - Relay mengirim ke
pengirimandengan header kunci idempoteneid, dengan retry dan backoff. Time out cukup diulang denganeidyang sama. - Konsumen idempoten.
pengirimanmenyimpaneiddenganUNIQUE. Panggilan kedua denganeidyang sama mengembalikan hasil pertama, bukan membuat pengiriman baru. - Rekonsiliasi. Tiap 5 menit cari pesanan
DIBAYARlebih dari 10 menit tanpa pengiriman, lalu terbitkan ulang. Pantau jumlahnya sebagai alarm.
Dari 300 pesanan yang time out per hari, semuanya akhirnya dikirim tepat satu kali secara efektif: event dikirim at-least-once, efeknya dibuat sekali lewat idempotensi. Bukan exactly-once di jaringan, melainkan exactly-once secara efek.
Terkait
- M05.05 Reservasi stok terbatas memakai transisi status yang sama untuk langkah awal alur ini.
- M05.01 Nyatakan invarians bisnis menjelaskan kenapa invarians lintas sistem perlu penjaga tambahan.
- M10 membahas outbox, change data capture, acknowledgment, dan konsumen idempoten lebih dalam.
- D05 (pembayaran dan ledger) melatih skenario ini. Rancangan itu belum tersedia.
Catatan
- Asumsi: broker pesan at-least-once dan database relasional per layanan.
- Angka 0,3% time out dan 100.000 pesanan adalah contoh hitung, bukan data nyata.
- Yang nggak dibahas: framework saga/orkestrasi, event sourcing, dan two-phase commit secara rinci.
- Terakhir ditinjau: 26 Oktober 2026.