🔬 Sesi 05:00 — Deep Learning
📚 Topik: Saga Pattern & Kompensasi — Transaksi Lintas Service Tanpa 2PC
Kategori: Distributed Systems · Level: Advanced · Asal konsep: Garcia-Molina & Salem, "Sagas", SIGMOD 1987
🎯 Tujuan Belajar
Setelah sesi ini, keputusan arsitektur berikut jadi lebih tajam:
- Kapan memecah satu transaksi bisnis jadi saga vs memaksa satu boundary service agar tetap satu transaksi lokal (seringkali jawaban yang benar: ubah boundary-nya, bukan tambah saga).
- Membedakan kompensasi dari rollback — dan tahu konsekuensi hukum/bisnis saat keduanya dianggap sama.
- Menentukan di mana pivot point berada, dan konsekuensi kalau salah menempatkannya.
- Memilih orchestration vs choreography berdasarkan topologi organisasi, bukan tren.
- Mendesain kompensasi yang idempoten dan aman terhadap timeout — penyebab insiden finansial paling umum di sistem saga.
🧠 Penjelasan Inti
Masalah yang diselesaikan
Transaksi bisnis jarang menghormati boundary service. "Buat order" menyentuh inventory, payment, shipping, loyalty — empat database, empat tim, kadang empat perusahaan. Atomicity klasik butuh 2PC: prepare di semua node, lalu commit.
2PC gagal untuk kasus ini karena empat alasan struktural:
| Sifat 2PC | Konsekuensi |
|---|---|
| Blocking | Coordinator mati setelah prepare → participant memegang lock sampai coordinator pulih. Lock = resource tak tersedia selama durasi itu. |
| Lock duration | Lock ditahan sepanjang seluruh transaksi. Transaksi lintas service berdurasi detik–menit, bukan milidetik. Throughput ambruk. |
| Heterogenitas | Butuh XA-compliant resource manager. Payment gateway pihak ketiga, message broker, object storage — tidak ada yang ikut XA. |
| Trust boundary | Tidak ada organisasi yang mau menyerahkan lock database-nya ke coordinator milik organisasi lain. |
Saga membalik asumsinya: jangan cegah kegagalan, urus setelah terjadi. Alih-alih lock, tiap langkah commit lokal segera dan tidak bisa ditarik. Kalau langkah ke-k gagal, jalankan langkah kompensasi C(k-1) … C(1) yang membatalkan efek bisnis-nya, bukan efek fisiknya.
Definisi formal yang penting
Saga = sekuens transaksi lokal T₁, T₂, …, Tₙ, dengan himpunan transaksi kompensasi C₁, C₂, …, C(n-1). Eksekusi valid bila:
- Semua Tᵢ sukses → saga sukses.
- Tₖ gagal → jalankan C(k-1), C(k-2), …, C₁ secara berurutan (backward recovery).
Jaminan yang didapat: ACD — Atomicity (semantik, bukan fisik), Consistency, Durability. Isolation hilang total. Ini bukan detail kecil; ini sumber seluruh kelas bug di sistem saga.
Kenapa isolation hilang itu fatal
Tanpa isolation, state antara terlihat oleh transaksi lain. Tiga anomali klasik (semua sudah dideskripsikan Garcia-Molina 1987, masih relevan persis):
| Anomali | Skenario |
|---|---|
| Dirty read | Customer lihat saldo sudah terpotong padahal saga belum commit — lalu belanja lagi dengan asumsi saldo itu final. |
| Fuzzy read / non-repeatable | Service A baca data, saga lain mengubahnya, A baca lagi → beda. Perhitungan A jadi tidak valid. |
| Lost update | Dua saga kompensasi berbeda menyentuh record yang sama; satu menimpa yang lain. |
Countermeasure desain (dari paper asli, masih dipakai):
- Semantic lock — tandai record "sedang diproses saga X" (
status = PENDING), tolak operasi konkuren yang bertentangan. Ini lock di level domain, bukan lock DB. Paling umum dipakai; harganya: butuh state tambahan + mekanisme expiry. - Commutative update — rancang operasi agar urutan tidak penting (mis. increment/decrement counter, bukan
SET balance = 100). - Pessimistic view — reorder langkah agar T yang bisa di-revert dieksekusi setelah T yang berisiko (lihat pivot).
- Reread value — baca ulang + validasi sebelum menulis.
- Version file — optimistik concurrency di level saga.
⚙️ Cara Kerja Internal
Klasifikasi langkah: sebelum dan sesudah pivot
Ini konsep yang paling sering dilewatkan, dan paling menentukan desain.
[Compensable] [Compensable] [PIVOT] [Retriable] [Retriable]
T1 T2 T3 T4 T5
↓ C1 ↓ C2 ↓ — ↓ — ↓ —
- Compensable (sebelum pivot): bisa dibatalkan semantik. Boleh gagal → trigger kompensasi mundur.
- Pivot: titik tak bisa kembali. Setelah T3 sukses, saga wajib maju sampai selesai — tidak ada opsi rollback lagi.
- Retriable (setelah pivot): dijamin sukses secara eventual (retry sampai berhasil, karena kegagalan hanya transient).
Konsekuensi: setelah pivot, kegagalan T4 tidak memicu kompensasi C2/C1. Yang terjadi adalah forward recovery — retry, eskalasi, atau intervensi manual. Kalau tim tidak sadar ini, mereka akan menulis handler "gagal → rollback semua" yang menghasilkan state mustahil.
Pivot biasanya adalah langkah dengan efek eksternal yang tidak bisa dibatalkan: pengiriman uang ke rekening pihak ketiga, penerbitan tiket pesawat, pengiriman fisik barang.
Orchestration
Satu komponen (saga orchestrator) memegang state machine dan memanggil service satu per satu.
┌──────────────────┐
│ Orchestrator │ ← state machine persisten (saga log)
│ saga_id: 8f3a │
└────┬──┬──┬──┬────┘
call │ │ │ │ call
▼ ▼ ▼ ▼
[Inv][Pay][Ship][Notify]
# pseudocode orchestrator
def run_saga(saga):
for step in saga.steps:
saga.log(step.name, "STARTED") # persist SEBELUM call
try:
result = step.execute(saga.ctx, idem_key=saga.id + step.name)
except PermanentError as e:
saga.log(step.name, "FAILED", e)
compensate_backwards(saga, from_step=step) # hanya jika step < pivot
return FAILED
except TransientError:
retry_with_backoff(step) # bukan kompensasi
continue
saga.log(step.name, "SUCCEEDED", result)
return COMPLETED
def compensate_backwards(saga, from_step):
for step in reversed(saga.completed_steps_before(from_step)):
# kompensasi WAJIB retry sampai sukses — tidak boleh menyerah diam-diam
while True:
try:
step.compensate(saga.ctx, idem_key=saga.id + step.name + ":comp")
break
except TransientError:
sleep_backoff()
except PermanentError:
dead_letter(saga, step) # antrean manual, alert, bukan drop
break
Dua detail yang membedakan implementasi benar dari implementasi mainan:
- Saga log ditulis sebelum efek eksternal (write-ahead intent). Kalau orchestrator crash setelah
charge()sukses tapi sebelum log, sistem tidak tahu harus mengompensasi apa. Pola ini identik dengan transactional outbox: satu transaksi DB lokal menulis intent + event, publisher terpisah mengirim. - Kompensasi tidak boleh "best effort". Kalau C gagal permanen, saga masuk status
NEEDS_ATTENTIONdi dead-letter queue dengan alert. Saga yang menelan kegagalan kompensasi = uang hilang tanpa jejak.
Choreography
Tidak ada coordinator. Tiap service mendengar event dan memutuskan sendiri.
OrderCreated → [Inventory] reserve → InventoryReserved
→ [Payment] charge → PaymentFailed
→ [Inventory] release
→ [Order] cancel
Keunggulan: tidak ada SPOF, coupling longgar, cocok lintas tim/organisasi. Kelemahan struktural: sulit mengetahui "apakah saga X selesai?". Tidak ada satu tempat yang memegang state. Setelah 6 bulan dan 20 event, graph-nya jadi siklik dan tidak bisa dipahami siapa pun. Ini alasan praktis kenapa banyak tim besar pindah balik ke orchestration begitu jumlah langkah > 5.
Hibrida yang lazim: choreography antar bounded context, orchestration di dalam satu context.
🏗️ Diagram: 2PC vs Saga vs TCC
2PC (lock-based)
A ─prepare─ B ─prepare─ C
A ◄─ready── B ◄─ready── C ← semua lock ditahan di sini
A ─commit─► B ─commit─► C
Kegagalan di titik lock = sistem beku sampai coordinator pulih.
SAGA (compensation-based)
T1 ─► T2 ─► T3 ✗
◄─ C2 ◄─ C1 ← tidak ada lock; efek bisnis dibatalkan belakangan
State antara terlihat publik. Butuh idempoten + semantic lock.
TCC (reservation-based)
Try1 ─► Try2 ─► Try3 ← resource di-RESERVE, belum dipakai
Confirm1 ─► Confirm2 ─► Confirm3 (atau Cancel1..3)
Isolation lebih baik (resource terkunci secara bisnis), tapi tiap service
wajib mendukung konsep "reservasi" — mahal untuk sistem legacy.
| Aspek | 2PC | Saga | TCC |
|---|---|---|---|
| Isolation | Penuh (lock) | Tidak ada | Parsial (reservasi) |
| Konsistensi | Kuat, instan | Eventual, semantik | Kuat saat Try selesai |
| Availability saat gagal | Blocking | Tetap jalan | Tetap jalan |
| Butuh dukungan bisnis | Tidak | Ya (kompensasi) | Ya (reservasi + expiry) |
| Lintas organisasi | Praktis tidak mungkin | Bisa | Sulit (pihak ketiga harus support) |
| Durasi transaksi wajar | Milidetik | Detik–hari | Menit–jam |
| Kompleksitas | Rendah (protokol) | Tinggi (desain domain) | Tinggi (dua fase bisnis) |
⚖️ Trade-off & Kapan JANGAN Dipakai
Bagian ini yang menentukan apakah kamu engineer senior atau cuma tahu polanya.
Jangan pakai saga kalau:
1. Bisa dihindari dengan mengubah boundary. Ini jawaban paling sering benar dan paling sering diabaikan. Kalau order + inventory + payment selalu berubah bersama dan konsistensinya harus instan — itu satu bounded context, bukan tiga. Saga adalah kompensasi untuk boundary yang salah. DDD bounded context memutuskan di mana transaksi harus konsisten kuat; saga dipakai antar context yang memang harus longgar.
Tanda boundary salah: saga dengan 3 langkah yang selalu dieksekusi berurutan dan tidak pernah punya jalur alternatif.
2. Butuh isolation nyata. Kalau ada constraint yang tidak boleh dilanggar sesaat pun (mis. kuota tidak boleh oversell 1 unit, saldo tidak boleh minus), saga tidak cukup. Semantic lock bisa membantu tapi menambah lock lagi — dan kamu kembali ke masalah 2PC dengan cara berbeda. Untuk kasus ini: single-writer per aggregate (satu service pemilik data), atau database dengan constraint kuat, atau reservation (TCC).
3. Langkahnya tidak punya invers semantik. Email yang sudah terkirim tidak bisa di-unsend. Uang yang sudah masuk rekening orang lain butuh transaksi balik baru — dengan biaya, waktu, dan kemungkinan gagal. Kalau mayoritas langkahmu irreversibel, saga hanya memberi ilusi atomicity. Yang benar: pindahkan langkah irreversibel ke setelah pivot, dan pastikan semua yang bisa gagal ada sebelumnya.
4. Tim belum sanggup operasionalnya. Saga = state machine persisten + dead-letter queue + rekonsiliasi manual + observability per saga_id + runbook. Kalau belum ada itu, saga akan gagal di production bukan karena desainnya salah, tapi karena tidak ada yang tahu cara menangani saga yang macet. Monolith modular dengan satu transaksi DB lebih murah dan lebih benar.
Jangan pakai choreography kalau:
- Langkah > ~5, atau ada branching/paralel dengan join.
- Butuh audit trail "apakah transaksi X selesai" untuk customer support atau compliance.
- Ada langkah yang butuh timeout global dengan deadline.
Biaya tersembunyi saga yang jarang dihitung:
| Biaya | Keterangan |
|---|---|
| State transien terlihat | Customer lihat "order dibuat" padahal payment belum. Butuh UI state eksplisit (PENDING, AWAITING_PAYMENT), bukan asumsi "kalau ada row berarti jadi". |
| Data tidak pernah "benar" sesaat | Laporan finance harus tahan terhadap saga setengah jalan. Rekonsiliasi jadi pekerjaan tetap, bukan incident. |
| Kompleksitas kompensasi = kompleksitas bisnis | Tiap langkah butuh analisis: apa invers-nya? apa yang terjadi kalau invers-nya gagal? siapa yang diberi tahu? |
| Latency | n panggilan serial + persist state tiap langkah. Saga 5 langkah = 5 round trip + 5 write. |
| Test surface meledak | n langkah → 2ⁿ jalur kegagalan. Yang realistis dites: tiap langkah gagal sekali, plus kompensasi itu sendiri gagal. |
🏭 Contoh Produksi & Failure Mode
Failure mode 1: Timeout → kompensasi ganda (paling umum, paling mahal)
Orchestrator: charge(payment, idem="saga-8f3a")
... timeout 5s, tidak ada response ...
→ anggap gagal → refund()
Padahal charge SUDAH sukses di provider, response-nya telat.
Hasil: customer ditagih DAN di-refund, atau di-refund dua kali.
Akar masalah: timeout bukan bukti kegagalan. Timeout hanya bukti "tidak tahu".
Mitigasi yang benar:
- Idempotency key di semua panggilan keluar (dan pada kompensasi, key berbeda: saga-8f3a:charge vs saga-8f3a:charge:refund).
- Inquiry sebelum kompensasi: pada timeout, panggil status endpoint provider dulu (GET /charges/{idem_key}). Kalau state tidak diketahui → jangan kompensasi, pertahankan di status UNKNOWN dan retry inquiry.
- Provider tanpa idempotency/inquiry (banyak payment gateway lama) → butuh tabel rekonsiliasi harian. Tidak ada jalan pintas.
Failure mode 2: Kompensasi non-idempoten
Kompensasi: "release inventory" diimplementasi sebagai `stock = stock + qty`
Dijalankan dua kali (retry setelah timeout) → stock naik 2× qty.
Perbaikan: stock = stock + qty WHERE reservation_id = ? (insert-if-absent), atau tabel reservasi dengan unique constraint. Aturan: kompensasi harus idempoten by construction, bukan by discipline.
Failure mode 3: Pivot salah tempat
Saga: reserve → charge → kirim email konfirmasi → create shipment. Charge = irreversibel (pivot sebenarnya), tapi diletakkan setelah "kirim email". Charge gagal → kompensasi mundur → email pembatalan dikirim ke customer yang belum pernah ditagih. Reputasi rusak karena urutan langkah, bukan bug logika.
Aturan: setiap langkah irreversibel atau berbiaya tinggi sedapat mungkin setelah semua langkah yang bisa gagal.
Failure mode 4: Orchestrator crash tanpa saga log
Orchestrator di memori, crash di tengah saga. Restart → state hilang → tidak ada yang tahu saga mana yang menggantung. Uang sudah ditransfer, order tidak pernah dibuat. Perbaikan: durable execution. Orchestrator harus menyimpan state ke DB (atau pakai engine yang melakukannya: Temporal, Camunda/Zeebe, AWS Step Functions, Seata). Saga log = source of truth, bukan memori proses.
Failure mode 5: Saga menggantung tanpa timeout global
Langkah tanpa deadline → saga IN_PROGRESS selamanya. Tidak ada yang mengompensasi, tidak ada yang menyelesaikan. Resource ter-reserve permanen (inventory ter-hold, kuota terpakai).
Perbaikan: deadline per langkah + deadline global per saga; sweeper job yang menemukan saga melewati deadline → eskalasi ke dead-letter, bukan auto-kompensasi buta.
Failure mode 6: Choreography siklik
OrderCancelled → InventoryReleased → PaymentRefunded → OrderCancelled (event handler me-republish karena tidak ada dedup). Loop tak terbatas.
Perbaikan: dedup by event id, saga_id sebagai korelasi, dan invariant eksplisit "satu saga_id hanya boleh transisi sekali per state".
Implementasi nyata (per 2024–2025, semantik stabil)
- Temporal — durable execution, workflow code ditulis sebagai fungsi biasa, state di-persist otomatis. Kompensasi =
try/exceptdengancompensate(). Paling dekat dengan "saga yang tidak bisa salah state". - Camunda 8 / Zeebe — BPMN 2.0 dengan compensation boundary event. Cocok kalau ada stakeholder bisnis yang perlu membaca alurnya.
- AWS Step Functions — state machine deklaratif,
Catch+Retryper state. - Seata (Alibaba, populer di ekosistem Java/Spring Cloud) — tiga mode: AT (auto-compensate via undo log), TCC, Saga.
- MassTransit / NServiceBus (
.NET) — state machine saga, persisten di EF Core/Redis.
Catatan penting: semua engine ini menyelesaikan koordinasi, bukan desain kompensasi. Kompensasi tetap keputusan domain yang harus kamu tulis sendiri. Tidak ada framework yang tahu cara membatalkan "kirim barang".
⚠️ Miskonsepsi Umum
"Kompensasi = rollback." Salah, dan berbahaya. Rollback mengembalikan state fisik ke titik sebelumnya, tak terlihat. Kompensasi membuat transaksi bisnis baru yang efeknya berlawanan — terlihat, tercatat, bisa gagal, bisa kena pajak, bisa butuh persetujuan. Refund bukan "un-charge"; refund adalah transaksi keuangan baru dengan jejak audit sendiri. Kalau model mental ini salah, seluruh desain kompensasi akan salah.
"Saga memberi eventual consistency, jadi cukup." Eventual consistency hanya soal state akhir. Saga tidak menjamin apa pun soal state antara. Dua customer bisa melihat snapshot berbeda di waktu yang sama, keduanya "benar". Kalau produk butuh read-your-writes, saga tidak memberikannya — butuh sticky routing, versioning, atau UI yang secara eksplisit menampilkan state transien.
"Choreography lebih loosely coupled, jadi lebih baik." Coupling tidak hilang, hanya berpindah dari interface call ke kontrak event — dan kontrak event lebih sulit di-evolve karena konsumen tidak terdaftar di mana pun. Orchestration membuat dependency eksplisit; choreography membuatnya implisit. Implisit ≠ longgar.
"Kalau tiap service sudah punya transaksi lokal, masalahnya selesai." Justru itu titik masalahnya. Transaksi lokal yang commit tidak bisa ditarik. Saga hanya mengelola konsekuensinya.
"Kompensasi harus jalan di jalur kegagalan saja."
Kompensasi juga dibutuhkan untuk pembatalan oleh user, timeout, dan koreksi manual oleh ops. Kalau kompensasi hanya hidup di catch block, tidak akan bisa dipanggil dari luar — dan tidak akan pernah dites.
"Saga butuh message broker." Tidak. Saga butuh state machine persisten + pemanggilan yang idempoten. Broker membantu reliability dan decoupling, tapi saga dengan HTTP call langsung + tabel state di Postgres adalah saga yang valid (dan seringkali cukup).
✅ Cek Pemahaman
1. Saga: reserveStock → chargeCard → createShipment. chargeCard timeout. Kamu tidak tahu apakah charge sukses. Apa tiga langkah berikutnya, berurutan, dan kenapa kompensasi langsung adalah jawaban yang salah?
2. Sebuah tim mendesain saga 7 langkah dengan choreography. Enam bulan kemudian tidak ada yang bisa menjawab "apakah order #12345 selesai?". Apa dua akar masalah strukturalnya, dan apa yang kamu ubah tanpa menulis ulang semua service?
3. Kenapa menempatkan langkah irreversibel (kirim uang, kirim barang) setelah pivot lebih aman daripada sebelum pivot — dan apa konsekuensi operasional yang harus kamu terima sebagai gantinya?
🔑 Poin Kunci
- Saga = ACD tanpa I. Atomicity-nya semantik, bukan fisik. Isolation hilang, dan itu bukan detail — itu sumber seluruh kelas bug.
- Kompensasi bukan rollback. Ini transaksi bisnis baru: terlihat, bisa gagal, butuh idempoten, butuh audit.
- Pivot point membagi saga jadi backward-recovery (sebelum) dan forward-recovery (sesudah). Salah tempat = insiden.
- Timeout ≠ gagal. Timeout = state tidak diketahui → inquiry, bukan kompensasi buta. Ini penyebab insiden finansial nomor satu.
- Saga log harus persisten dan ditulis sebelum efek eksternal. Orchestrator tanpa durable state = saga yang hilang saat crash.
- Orchestration vs choreography: pilih berdasarkan kebutuhan auditability dan jumlah langkah, bukan tren.
- Jawaban paling sering benar untuk "butuh saga" adalah "boundary-mu salah". Periksa itu dulu sebelum menulis state machine.
➡️ Eksplorasi Lanjut
- Transactional outbox + CDC — bagaimana menjamin event saga benar-benar terkirim tanpa 2PC antara DB dan broker (Debezium, Postgres logical replication).
- Idempotency key di level HTTP — desain tabel idempotensi, TTL, dan penanganan concurrent request dengan key sama (bandingkan pendekatan Stripe vs implementasi sendiri).
- Durable execution — baca model Temporal: kenapa workflow code bisa ditulis sebagai fungsi imperatif tapi tetap deterministik saat replay; bandingkan dengan state machine deklaratif.
- Konsistensi model — linearizability vs serializability vs strict serializability; kenapa "eventually consistent" adalah label yang terlalu kasar untuk mendeskripsikan sistem nyata (Jepsen menjadi bacaan wajib).
- Saga vs process manager — di DDD, process manager adalah konsep lebih umum; saga hanyalah satu bentuknya. Kapan kamu butuh process manager yang mengoordinasikan banyak aggregate lintas waktu?
- Rekonsiliasi — pola deteksi state divergen (double-entry ledger, nightly diff) sebagai jaring pengaman ketika saga dan kompensasi sama-sama gagal.