← semua materi
05:00 Arsitektur · 02 Nov 2026 · 05:01

🔬 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:

  1. 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).
  2. Membedakan kompensasi dari rollback — dan tahu konsekuensi hukum/bisnis saat keduanya dianggap sama.
  3. Menentukan di mana pivot point berada, dan konsekuensi kalau salah menempatkannya.
  4. Memilih orchestration vs choreography berdasarkan topologi organisasi, bukan tren.
  5. 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:

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):


⚙️ 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        ↓ —        ↓ —          ↓ —

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:

  1. 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.
  2. Kompensasi tidak boleh "best effort". Kalau C gagal permanen, saga masuk status NEEDS_ATTENTION di 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:

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)

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


➡️ Eksplorasi Lanjut

  1. Transactional outbox + CDC — bagaimana menjamin event saga benar-benar terkirim tanpa 2PC antara DB dan broker (Debezium, Postgres logical replication).
  2. Idempotency key di level HTTP — desain tabel idempotensi, TTL, dan penanganan concurrent request dengan key sama (bandingkan pendekatan Stripe vs implementasi sendiri).
  3. Durable execution — baca model Temporal: kenapa workflow code bisa ditulis sebagai fungsi imperatif tapi tetap deterministik saat replay; bandingkan dengan state machine deklaratif.
  4. Konsistensi model — linearizability vs serializability vs strict serializability; kenapa "eventually consistent" adalah label yang terlalu kasar untuk mendeskripsikan sistem nyata (Jepsen menjadi bacaan wajib).
  5. 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?
  6. Rekonsiliasi — pola deteksi state divergen (double-entry ledger, nightly diff) sebagai jaring pengaman ketika saga dan kompensasi sama-sama gagal.