1. Masalahnya: Tidak Ada Lagi Transaksi Global
Di MONOLITH -- satu database, satu transaksi: BEGIN; INSERT INTO pesanan ...; UPDATE stok SET jumlah = jumlah - 1 ...; INSERT INTO pembayaran ...; COMMIT; -- semua berhasil, atau semua batal Di MICROSERVICES -- tiga database terpisah: Layanan Pesanan -> db-pesanan Layanan Stok -> db-stok Layanan Pembayaran -> db-pembayaran Tidak ada BEGIN/COMMIT yang mencakup ketiganya.
2PC secara teori memberi transaksi lintas database, tetapi harganya terlalu mahal: ia memblokir β semua peserta mengunci sumber daya sampai koordinator memutuskan. Bila koordinator mati di tengah fase kedua, peserta terkunci tanpa kepastian. Ia juga menurunkan ketersediaan secara drastis karena sistem hanya sekuat peserta paling lambat, dan sebagian besar teknologi modern (Kafka, sebagian besar NoSQL, banyak API pihak ketiga) tidak mendukungnya sama sekali.
2. Saga: Rangkaian Transaksi Lokal
Alih-alih satu transaksi besar, saga memecahnya menjadi beberapa transaksi lokal. Bila ada langkah yang gagal, saga menjalankan transaksi kompensasi untuk membatalkan langkah-langkah sebelumnya.
Alur normal: 1. Buat pesanan (status: MENUNGGU) 2. Pesan stok 3. Tagih pembayaran 4. Tandai pesanan LUNAS Bila langkah 3 gagal: 3'. (gagal) 2'. Kompensasi: lepaskan pesanan stok 1'. Kompensasi: tandai pesanan DIBATALKAN
Rollback menghapus jejak seolah tidak pernah terjadi. Kompensasi adalah tindakan bisnis baru yang meniadakan efek sebelumnya, dan jejaknya tetap tercatat. Anda tidak bisa "membatalkan" email yang sudah terkirim β Anda mengirim email pembatalan. Anda tidak bisa membatalkan penarikan dana yang sudah masuk buku besar β Anda mencatat pengembalian dana. Rancang alur bisnisnya dengan kesadaran ini.
3. Saga Orkestrasi
Ada satu komponen koordinator yang memerintahkan setiap langkah dan mengetahui keseluruhan alur.
ββββββββββββββββββββββββ
β Orkestrator Pesanan β
ββββββββββββββββββββββββ
β β β
1. βΌ 2. βΌ 3. βΌ
ββββββββββ ββββββββ βββββββββββββ
β Pesananβ β Stok β β Pembayaranβ
ββββββββββ ββββββββ βββββββββββββ
Orkestrator memegang state machine:
MENUNGGU -> STOK_DIPESAN -> DIBAYAR -> SELESAI
β β
DIBATALKAN <ββββββ
| Kelebihan | Kekurangan |
|---|---|
| Alur terlihat jelas di satu tempat | Ada titik pusat yang bisa jadi hambatan |
| Mudah di-debug dan dipantau | Orkestrator bisa membengkak menjadi "monolith terselubung" |
| Logika kompensasi terpusat | Layanan menjadi lebih pasif, kurang otonom |
| Cocok untuk alur bercabang yang rumit | Perlu komponen tambahan untuk dikelola |
4. Saga Koreografi
Tidak ada koordinator. Tiap layanan mendengarkan event dan bereaksi menerbitkan event berikutnya.
Pesanan ββPesananDibuatββ> Stok ββStokDipesanββ> Pembayaran
β
Pesanan <ββPembayaranLunasββββββββββββββββββββββββββββ
Jalur kegagalan:
Pembayaran ββPembayaranGagalββ> Stok ββStokDilepasββ> Pesanan
ββ> DIBATALKAN
| Kelebihan | Kekurangan |
|---|---|
| Layanan benar-benar terpisah (loosely coupled) | Tidak ada tempat melihat alur secara utuh |
| Tidak ada titik pusat kegagalan | Sangat sulit di-debug saat ada masalah |
| Mudah menambah layanan pendengar baru | Risiko ketergantungan melingkar antar event |
| Cocok untuk alur linier sederhana | Alur bercabang cepat menjadi kacau |
Gunakan koreografi untuk alur yang pendek dan linier dengan 2β4 layanan. Gunakan orkestrasi begitu ada percabangan, langkah bersyarat, atau lebih dari empat layanan. Banyak tim memulai dengan koreografi karena terasa lebih "murni", lalu terpaksa berpindah ke orkestrasi setelah tidak ada seorang pun yang bisa menjelaskan bagaimana sebuah pesanan berakhir dalam keadaan aneh.
5. Masalah Dual Write dan Pola Outbox
await db.simpan(pesanan); // β
berhasil
await broker.terbitkan('PesananDibuat'); // β proses mati di sini
// Pesanan tersimpan, tapi TIDAK ADA yang tahu.
// Stok tidak pernah dipesan. Saga berhenti diam-diam.
Membalik urutannya tidak menolong β Anda hanya menukar masalahnya menjadi event yang terbit untuk pesanan yang tidak pernah tersimpan.
Tulis data bisnis dan event ke database yang sama, dalam satu transaksi. Proses terpisah kemudian membaca tabel outbox dan menerbitkannya ke broker.
BEGIN;
INSERT INTO pesanan (id, pengguna_id, total, status)
VALUES ('p-1001', 55, 250000, 'MENUNGGU');
INSERT INTO outbox (id, jenis_agregat, id_agregat, jenis_event, payload)
VALUES (gen_random_uuid(), 'pesanan', 'p-1001', 'PesananDibuat',
'{"id":"p-1001","total":250000}');
COMMIT;
-- Keduanya berhasil, atau keduanya batal. Tidak ada keadaan tengah.
1. POLLING PUBLISHER Proses latar membaca baris outbox yang belum terkirim, menerbitkannya, lalu menandainya terkirim. Sederhana, tapi menambah beban baca ke database. 2. CHANGE DATA CAPTURE (CDC) Alat seperti Debezium membaca write-ahead log database dan menerbitkan perubahan secara otomatis. Tanpa polling, latensi rendah, tapi perlu komponen tambahan.
6. Idempotensi Wajib di Setiap Langkah
Broker pesan memberi jaminan at-least-once. Setiap langkah saga bisa menerima perintah yang sama dua kali.
async function tanganiPesanStok(perintah) {
// Kunci idempotensi = ID saga + nama langkah
const kunci = perintah.sagaId + ':pesan-stok';
const sudah = await db.cariLangkahSelesai(kunci);
if (sudah) return sudah.hasil; // sudah pernah, kembalikan hasil lama
const hasil = await pesanStok(perintah.produkId, perintah.jumlah);
await db.simpanLangkahSelesai(kunci, hasil);
return hasil;
}
Pesan kompensasi bisa terkirim ganda juga. "Lepaskan 1 stok" yang dijalankan dua kali akan mengembalikan 2 stok β dan sekarang stok Anda salah. Kompensasi juga wajib memeriksa apakah ia sudah pernah dijalankan.
7. Konsekuensi yang Harus Diterima Bisnis
Di tengah saga, sistem berada dalam keadaan yang terlihat oleh pengguna lain: pesanan sudah ada tapi belum dibayar, stok sudah berkurang tapi pesanan bisa saja dibatalkan. Ini tidak bisa dihindari β ini harga dari melepaskan transaksi global.
Cara menanganinya: gunakan status yang eksplisit (MENUNGGU_PEMBAYARAN, bukan sekadar AKTIF) agar bagian lain sistem tahu keadaan itu sementara; terapkan semantic lock dengan menandai record sebagai sedang diproses; dan rancang tampilan pengguna agar jujur menampilkan "sedang diproses" alih-alih berpura-pura sudah selesai.
8. Memantau Saga
- Saga yang menggantung. Beri batas waktu tiap langkah. Saga yang berada di satu status lebih dari X menit harus memicu alert.
- Tingkat kompensasi. Naiknya jumlah saga yang terkompensasi adalah gejala paling awal ada dependensi yang bermasalah.
- Correlation ID. Satu ID yang mengalir di semua layanan dan semua event, sehingga seluruh perjalanan satu pesanan bisa dilacak di log dan trace.
- Dead letter. Perintah saga yang gagal berulang harus masuk antrian khusus untuk diperiksa manusia, bukan diulang selamanya.