Saga adalah cara menjalankan satu proses bisnis yang menyentuh beberapa service tanpa satu transaksi besar: proses itu dipecah jadi urutan transaksi lokal, satu per service, dan tiap langkah punya compensating action (kompensasi) yang membatalkannya. Kalau satu langkah gagal, langkah-langkah yang sudah sukses dibatalkan satu per satu dengan urutan terbalik. Bayangin kamu merencanakan liburan: pesan hotel, lalu tiket pesawat, lalu sewa mobil. Tiga perusahaan, tiga sistem, gak ada satu tombol "batalkan semuanya". Kalau sewa mobil ditolak, kamu sendiri yang membatalkan tiket pesawat, lalu hotel. Tiap pesanan punya cara batalnya sendiri, dan pembatalannya juga sebuah transaksi baru (refund), bukan menghapus sejarah.
🧪 Tebakan cepat: checkout pesanan #1001 sudah menahan kursi A-12 dan A-13, lalu memotong kartu Budi Rp1.500.000, tapi service
tiketmenolak menerbitkan e-tiket. Mana yang harus dibatalkan duluan: kursinya atau pembayarannya? Tebak dulu, jawabannya ada di bagian "Gagal di langkah 3: kompensasi urutan terbalik".
Checkout TiketNada: tiga service, tiga database
Di lesson Distributed Transactions kita lihat kenapa two-phase commit gak cocok untuk TiketNada: semua peserta terkunci menunggu satu coordinator. Jadi checkout dijalankan sebagai saga. Tiap service punya database sendiri, dan tiap langkah langsung commit di database-nya:
| Langkah | Service | Aksi (transaksi lokal) | Kompensasi |
|---|---|---|---|
| 1 | konser |
TahanKursi: A-12, A-13 jadi DITAHAN |
LepasKursi |
| 2 | bayar |
PotongKartu: Rp1.500.000 |
Refund Rp1.500.000 |
| 3 | tiket |
TerbitkanTiket: 2 e-tiket, NIK pemegang dicek |
BatalkanTiket |
Kompensasi bukan "undo" ajaib. Kartu yang sudah terpotong gak bisa dibuat seolah gak pernah terpotong; yang bisa dilakukan adalah transaksi baru yang mengembalikan uangnya. Itu sebabnya disebut compensating transaction.
Gagal di langkah 3: kompensasi urutan terbalik
Di model contoh ini, satu jalan di jaringan butuh 10 ms, konser bekerja 20 ms, bayar 120 ms (memotong kartu) dan 100 ms (refund), tiket 60 ms. Service order jadi pengatur, dan NIK pemegang tiket kedua ternyata gak valid:
| ms | Pesan | Akibatnya |
|---|---|---|
| 0 | order -> konser: TahanKursi |
|
| 40 | konser -> order: KursiDitahan |
kursi tersedia 10.000 jadi 9.998 |
| 180 | bayar -> order: KartuTerpotong |
kartu Budi -Rp1.500.000 |
| 260 | tiket -> order: ditolak, 422 NIK gak valid |
e-tiket gak terbit |
| 380 | bayar -> order: RefundSelesai |
kartu Budi kembali Rp0 |
| 420 | konser -> order: KursiDilepas |
kursi kembali 10.000 |
Jawaban tebakannya: pembayarannya dulu, baru kursinya. Langkah yang terakhir sukses dibatalkan pertama. Kenapa? Karena tiap keadaan di tengah jalan jadi keadaan yang memang pernah ada saat saga maju: "kursi ditahan, kartu terpotong", lalu "kursi ditahan, kartu utuh", lalu "kosong". Kalau kursi dilepas duluan, ada saat kartu Budi terpotong padahal kursinya sudah bisa dibeli orang lain.
Aturannya berlaku untuk urutan apa pun: kalau sebuah saga menjalankan charge, lalu reserve stok, lalu shipping, dan shipping gagal, maka stok di-release dulu, baru pembayaran di-refund. Langkah yang gagal sendiri gak perlu dikompensasi: transaksi lokalnya gak pernah commit.
Kode orchestrator
// Saga checkout #1001 (orkestrasi): tiket menolak di langkah 3
const db = { kursi: 10000, kartuBudi: 0, eTiket: 0 };
const langkah = [
{ svc: "konser", aksi: "TahanKursi", undo: "LepasKursi",
jalan: () => (db.kursi -= 2), batal: () => (db.kursi += 2) },
{ svc: "bayar", aksi: "PotongKartu", undo: "Refund",
jalan: () => (db.kartuBudi -= 1500000), batal: () => (db.kartuBudi += 1500000) },
{ svc: "tiket", aksi: "TerbitkanTiket", undo: "BatalkanTiket",
jalan: () => { throw new Error("422 NIK pemegang tiket gak valid"); }, batal: () => (db.eTiket -= 2) },
];
function sagaCheckout() {
const selesai = []; // langkah yang sudah commit, urut
for (const l of langkah) {
try {
l.jalan();
console.log(l.svc + ": " + l.aksi + " sukses");
selesai.push(l);
} catch (err) {
console.log(l.svc + ": " + l.aksi + " ditolak, " + err.message);
for (const s of selesai.reverse()) {
s.batal(); // kompensasi, urutan terbalik
console.log(s.svc + ": " + s.undo);
}
return "BATAL";
}
}
return "LUNAS";
}
console.log("pesanan #1001:", sagaCheckout());
console.log(db); // { kursi: 10000, kartuBudi: 0, eTiket: 0 }
Output-nya: konser: TahanKursi sukses, bayar: PotongKartu sukses, tiket: TerbitkanTiket ditolak, 422 NIK pemegang tiket gak valid, bayar: Refund, konser: LepasKursi, pesanan #1001: BATAL.
- Tiap item
langkahmemasangkan aksi dengan kompensasinya. Saga tanpa kompensasi untuk satu langkah berarti langkah itu gak bisa dibatalkan. selesaimencatat langkah yang sudah commit. Cuma itu yang dikompensasi.selesai.reverse()membalik urutannya: Refund dulu, LepasKursi kemudian.- Di sistem sungguhan,
ordermenyimpan posisi saga ke database setiap dapat jawaban (state machine). Kalau prosesnya crash setelah KartuTerpotong, saat hidup lagi ia tahu harus lanjut ke langkah 3, bukan mulai dari awal.
Orkestrasi vs koreografi
Ada dua cara mengatur siapa memanggil siapa:
- Orkestrasi (orchestration): satu pengatur pusat, di sini
OrderSagadi serviceorder, mengirim command ke tiap service, menunggu jawabannya, dan memutuskan kompensasi. Alurnya eksplisit di satu file, dan state tiap saga bisa dibaca di satu tabel. - Koreografi (choreography): gak ada pengatur. Tiap service mendengar event dan bereaksi sendiri, seperti di lesson Event-Driven Architecture:
konsermendengar PesananDibuat lalu publish KursiDitahan,bayarmendengar KursiDitahan, dan seterusnya. Saat gagal,tiketpublish TiketDitolak,bayarmendengarnya lalu refund,konsermendengar PembayaranDikembalikan lalu melepas kursi.
Untuk kegagalan yang sama, orkestrasi butuh 10 pesan (5 command, 5 jawaban) dan selesai di ms 420. Koreografi cuma 6 event dan selesai di ms 380. Harganya: aturan "kalau TiketDitolak, refund" tinggal di bayar, aturan "kalau PembayaranDikembalikan, lepas kursi" tinggal di konser. Untuk tahu pesanan #1001 sedang di mana, kamu harus merangkai event dari empat service.
Pegangan praktisnya: koreografi untuk rantai pendek (2 sampai 3 langkah) yang ingin service-nya saling gak kenal; orkestrasi untuk saga lebih dari 3 langkah, atau saat tim sering kesulitan menelusuri di langkah mana sebuah pesanan gagal. Flow yang eksplisit lebih mudah di-debug.
Kompensasi harus idempoten
Ingat lesson Kenapa Distributed System Sulit: response bisa hilang di jalan. Misalnya bayar sudah me-refund Rp1.500.000 di ms 370, tapi jawabannya hilang. order menunggu 2.000 ms, timeout, lalu mengirim Refund lagi. Tanpa pengaman, bayar me-refund lagi: Budi menerima Rp3.000.000 untuk pembayaran Rp1.500.000. Di koreografi lebih parah: event TiketDitolak dikirim ulang broker, bayar publish PembayaranDikembalikan dua kali, dan konser melepas kursi dua kali. Kursi tersedia jadi 10.002, dua kursi hantu yang bisa dijual.
Obatnya sama dengan lesson Idempotency & Retry Safety: tiap kompensasi membawa Idempotency-Key, misalnya refund-1001. bayar menyimpan hasil per key; kiriman kedua dengan key yang sama cuma mendapat hasil yang tersimpan, uangnya gak keluar lagi. Kompensasi yang idempoten boleh dipanggil berkali-kali dengan hasil yang sama, dan itu wajib, karena retry pasti terjadi.
Kalau kompensasinya gagal terus (misalnya bank down), jangan menyerah diam-diam: retry dengan jeda, lalu pindahkan ke dead letter queue untuk dicek manusia (lesson Message Queue).
📌 Saga gak punya isolation
Transaksi database biasa menyembunyikan perubahan setengah jadi dari orang lain. Saga gak bisa: begitu langkah 1 commit, pembeli lain sudah melihat A-12 sebagai DITAHAN. Karena itu keadaan tengah dibuat eksplisit: kursi berstatus DITAHAN (bukan TERJUAL) dan pesanan berstatus MENUNGGU_BAYAR sampai saga selesai. Halaman lain membaca status itu dan gak menganggapnya final. Langkah yang paling mungkin ditolak sebaiknya ditaruh di depan, dan langkah yang gak bisa dibatalkan (mengirim email) ditaruh paling akhir.
🎯 Checklist saga
- State saga disimpan ke database tiap langkah, supaya crash bisa dilanjutkan.
- Correlation ID (di sini nomor saga #1001) di setiap command dan event, supaya satu saga bisa ditelusuri lintas service (lesson Observability in Distributed Systems).
- Kompensasi idempoten dan dites, bagian yang paling sering bug.
- Timeout di tiap langkah: jangan menunggu selamanya.
- Dead letter queue untuk kompensasi yang tetap gagal.
⚠️ Jebakan yang sering ketemu
- Kompensasi yang gak idempoten. Retry setelah timeout berarti refund dobel atau kursi hantu.
- Kompensasi dengan urutan yang sama, bukan terbalik. Melepas kursi sebelum refund membuat ada saat Budi bayar tanpa kursi.
- State saga cuma di memori. Orchestrator crash, saga hilang: kursi tertahan dan uang terpotong tanpa ada yang menyelesaikan.
- Menganggap kompensasi sama dengan menghapus baris. Refund adalah transaksi baru dengan catatannya sendiri; jejak pembayaran pertama tetap ada.
TL;DR: Saga menjalankan proses lintas service sebagai urutan transaksi lokal, masing-masing dengan kompensasi. Checkout #1001: TahanKursi (konser), PotongKartu (bayar), TerbitkanTiket (tiket). Kalau tiket menolak, langkah yang sudah sukses dibatalkan dengan urutan terbalik: Refund dulu, LepasKursi kemudian; langkah yang gagal gak perlu dikompensasi. Orkestrasi: satu pengatur mengirim command, alurnya eksplisit dan mudah di-debug, cocok untuk saga lebih dari 3 langkah. Koreografi: service bereaksi pada event, lebih sedikit pesan dan lebih lepas, tapi alurnya tersebar, cocok untuk rantai pendek. Kompensasi wajib idempoten (Idempotency-Key seperti refund-1001), karena retry setelah timeout bisa me-refund dua kali. Simpan state saga ke database, pasang correlation ID dan timeout, dan kirim kompensasi yang tetap gagal ke dead letter queue.