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:
- Hindari distributed transactions jika bisa — rancang batas service agar transaksi tetap lokal
- Gunakan Saga untuk lintas service di microservices
- Gunakan idempotency agar retry aman
- Design for failure — asumsikan setiap step bisa gagal
- 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.
🎭 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:
- Saga lupa kompensasi step = inconsistent state. Wajib design compensating action untuk setiap step
- Compensation tidak idempotent = retry refund 2x = double refund
- Saga panjang ribet di-trace = wajib correlation ID + state machine visualization
- 2PC di production cross-service = blocking forever kalau coordinator crash. Pakai cuma di DB cluster sama
- Lupa edge case "compensation failed" — handle dead-letter manual review
🎯 Pilih distributed transaction strategy:
- Single DB ACID → ACID transaction (default)
- Multi-service async → Saga (orchestration atau choreography)
- Critical financial multi-party → 2PC + manual reconciliation fallback
- Read-mostly cross-service → eventual consistency + view materialization
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.