Distributed Transactions — System Design

Distributed Transactions adalah transaksi yang melibatkan lebih dari satu database atau service. Ini adalah salah satu masalah tersulit dalam system design. Men

Distributed Transactions adalah transaksi yang melibatkan lebih dari satu database atau service. Ini adalah salah satu masalah tersulit dalam system design.

Mengapa sulit?

Transaksi lokal bisa atomic — semua berhasil atau semua gagal. Tapi ketika melibatkan beberapa sistem yang terpisah via jaringan, network bisa putus di tengah proses.

Transfer bank: Saldo A dikurangi ✓ → Network putus → Saldo B tidak bertambah ✗
               Uang hilang begitu saja!

Solusi 1: Two-Phase Commit (2PC)

Phase 1 — Voting:
  Coordinator mengirim "PREPARE" ke semua participant
  Setiap participant:
    → Lock resource yang diperlukan
    → Tulis ke WAL (write-ahead log)
    → Balas "READY" atau "ABORT"

Phase 2 — Decision:
  Jika semua READY → Coordinator kirim "COMMIT"
  Jika ada ABORT   → Coordinator kirim "ROLLBACK"
// Pseudo-code 2PC
class TwoPhaseCommit {
  async execute(participants, operation) {
    // Phase 1: Prepare
    const votes = await Promise.all(
      participants.map(p => p.prepare(operation))
    );

    if (votes.every(v => v === "READY")) {
      // Phase 2: Commit
      await Promise.all(participants.map(p => p.commit()));
      return "COMMITTED";
    } else {
      // Phase 2: Rollback
      await Promise.all(participants.map(p => p.rollback()));
      return "ABORTED";
    }
  }
}

Masalah 2PC: jika coordinator crash setelah Phase 1, participant bisa terkunci selamanya menunggu keputusan.

Solusi 2: Saga Pattern

Serangkaian transaksi lokal. Jika gagal, jalankan "compensating transactions":

Choreography Saga (event-based):
  Order Service → "order.created"
                    → Inventory Service reserve stok → "stock.reserved"
                    → Payment Service charge        → "payment.done"
                    → Shipping Service ship         → "order.shipped"

  Jika payment gagal → publish "payment.failed"
                        → Inventory Service release stok (kompensasi)
// Orchestration Saga (coordinator):
class OrderSaga {
  steps = [
    {
      execute: (ctx) => inventoryService.reserve(ctx.items),
      compensate: (ctx) => inventoryService.release(ctx.items),
    },
    {
      execute: (ctx) => paymentService.charge(ctx.userId, ctx.amount),
      compensate: (ctx) => paymentService.refund(ctx.userId, ctx.amount),
    },
    {
      execute: (ctx) => shippingService.ship(ctx.orderId),
      compensate: () => shippingService.cancel(ctx.orderId),
    },
  ];

  async run(context) {
    const completed = [];
    for (const step of this.steps) {
      try {
        await step.execute(context);
        completed.push(step);
      } catch (err) {
        // Kompensasi mundur dari step yang sudah selesai
        for (const done of completed.reverse()) {
          await done.compensate(context);
        }
        throw err;
      }
    }
  }
}

Perbandingan 2PC vs Saga:

Aspek 2PC Saga
Konsistensi Strong Eventual
Blocking Ya (lock resource) Tidak
Kompleksitas Protokol kompleks Kompensasi kompleks
Performance Lebih lambat Lebih cepat
Cocok untuk Database cluster sama Lintas microservice
Failure recovery Sulit jika coordinator mati Lebih mudah

Best practices:

  1. Hindari distributed transactions jika bisa — rancang batas service agar transaksi tetap lokal
  2. Gunakan Saga untuk lintas service di microservices
  3. Gunakan idempotency agar retry aman
  4. Design for failure — asumsikan setiap step bisa gagal
  5. Gunakan Outbox Pattern untuk reliable event publishing
Prinsip: "Design for failure, not for success"
Setiap langkah distributed transaction harus bisa di-retry
dan dicompensate dengan aman.
<title>2PC vs Saga</title> Two-Phase Commit (2PC) — Coordinator + Participants Coordinator Participant A Participant B Participant C PREPARE (phase 1) YES / NO (vote) COMMIT (phase 2) Blocking: semua participant lock resource sampai phase 2. Coordinator crash = deadlock. Saga — Rangkaian Transaksi Lokal + Kompensasi Reserve Charge Ship step 1 step 2 step 3 Ship fails! Compensate: refund charge, release reservation (mundur dari langkah terakhir)
2PC butuh coordinator dan lock global (blocking, strong consistency). Saga menjalankan transaksi lokal berurutan dan memanggil compensating transaction saat ada langkah yang gagal (non-blocking, eventual consistency).

🎭 Analogi sehari-hari: Order Tokopedia. Step 1: stok dipotong. Step 2: bayar. Step 3: kirim. Step 3 gagal (kurir gak ada) → kompensasi: refund duit (undo step 2), tambahin stok lagi (undo step 1). Itu Saga. 2PC: kasir tahan semua langkah sampai konfirm semua bisa, kalau salah satu ragu = batalin semua. Lebih aman tapi blocking.

💡 Saga = pragmatic distributed transaction: Real-world distributed system hampir selalu pakai Saga, bukan 2PC. Alasan: 2PC blocking + butuh coordinator yang gak boleh mati. Saga eventual consistent tapi resilient terhadap failure node.

⚠️ Jebakan klasik:

🎯 Pilih distributed transaction strategy:

TL;DR: Distributed transaction sulit. 2PC blocking + butuh coordinator. Saga pragmatic default: rangkaian local transaksi + kompensasi kalau gagal. Wajib idempotent + correlation tracking. Real-world: hampir selalu Saga.