Kodeloka
  1. Kodeloka
  2. Kelas Microservices
  3. Materi

Orkestrasi Β· Modul 12 dari 24

Saga Pattern & Transaksi Terdistribusi

Menjaga konsistensi data lintas layanan tanpa transaksi ACID global: saga orkestrasi, saga koreografi, transaksi kompensasi, dan pola Outbox.

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.
🚫 Kenapa Two-Phase Commit (2PC) dihindari

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
⚠️ Kompensasi bukan rollback

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 <β”€β”€β”€β”€β”€β”˜
KelebihanKekurangan
Alur terlihat jelas di satu tempatAda titik pusat yang bisa jadi hambatan
Mudah di-debug dan dipantauOrkestrator bisa membengkak menjadi "monolith terselubung"
Logika kompensasi terpusatLayanan menjadi lebih pasif, kurang otonom
Cocok untuk alur bercabang yang rumitPerlu 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
KelebihanKekurangan
Layanan benar-benar terpisah (loosely coupled)Tidak ada tempat melihat alur secara utuh
Tidak ada titik pusat kegagalanSangat sulit di-debug saat ada masalah
Mudah menambah layanan pendengar baruRisiko ketergantungan melingkar antar event
Cocok untuk alur linier sederhanaAlur bercabang cepat menjadi kacau
πŸ“Œ Panduan memilih

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

Kode yang tampak benar tapi rusak
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.

πŸ’‘ Solusinya: Pola Outbox

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.
Dua cara mengirim dari outbox
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;
}
⚠️ Kompensasi juga harus idempoten

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

🚫 Saga tidak memberi isolasi

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.