Menyelesaikan transaksi lintas layanan yang tak bisa ACID dengan saga: membandingkan orchestration dan choreography, menerapkan compensating transactions untuk pembayaran gagal, dan mengimplementasikan state per-step lengkap dengan timeout di order-service

Episode 12 memberi kita outbox — event tidak bisa hilang lagi. Tapi ada masalah yang belum terselesaikan: transaksi lintas layanan. Saat checkout, "sukses" berarti reserve stok di product-service ✓, buat order di order-service ✓, dan bayar di payment-service ✓ — tiga database berbeda. Tidak ada database yang bisa meng-commit ketiganya sekaligus. ACID satu database mustahil di sini.
Jawabannya bukan distributed transaction berat (2PC), melainkan saga: rangkaian transaksi lokal yang dihubungkan, dengan kompensasinya masing-masing. Episode ini membangun pola yang sudah lewat sekilas berkali-kali — sekarang kita formalisasi.
Two-Phase Commit (2PC) — prepare semua, commit semua — sering dipakai di SOA. Pada microservices ia tidak disukai: semua node harus hidup & merespons dalam jendela waktu; satu databse lambat menahan seluruh transaksi; dan dalam sistem terdistribusi ada kegagalan yang tak terduga (partisi jaringan) yang bisa membuat 2PC terkunci. Saga menerima kenyataan: eventual consistency dengan kompensasi eksplisit.
Saga adalah urutan transaksi lokal T1...Tn di mana tiap Ti punya kompensasi Ci untuk membatalkan efeknya. Dua gaya utama:
Tiap layanan mengonsumsi event, mengerjakan bagiannya, lalu mempublish event lanjutan. Yang dipakai tokokita:
Kelebihan: sangat decoupled, tidak ada service yang "mengatur" yang lain. Kekurangan: alur tersebar di banyak konsumen — sulit dilacak "sampai mana transaksi ini?" tanpa log lintas service; satu consumer salah, rantai berhenti diam-diam.
Satu orchestrator (biasanya order-service) yang mencatat step, memanggil/event-kan ke layanan lain, dan menangani kompensasi terpusat. Kelebihan: alur jelas, transisi bisa diaudit. Kekurangan: orchestrator bisa jadi bottleneck & monolith kedua bila menampung logika semua langkah.
| Aspek | Choreography | Orchestration |
|---|---|---|
| Coupling | Sangat decoupled | Orkestrator tahu semua |
| Lacak status | Tersebar | Terpusat di orkestrator |
| Kompensasi | Di tiap konsumen | Orkestrator yang memutuskan |
| Cocok | Alur sederhana & stabil | Alur kompleks, butuh audit |
| Risiko | Rantai patah diam-diam | Orkestrator jadi SPOF |
Note
Tokokita mengambil jalur campuran yang pragmatis: choreography untuk transisi biasa (order.confirmed → payment.succeeded → order PAID) karena alurnya lurus dan stabil, ditambah orchestrator ringan di order-service untuk keputusan kompensasi & timeout — dicatat detail di bawah. Saat saga tumbuh rumit (multi-tahap fulfillment), pertimbangkan orkestrasi murni dengan Saga Execution Framework atau Dapr workflow (episode 25).
Aturan emas saga: setiap langkah harus punya kompensasi. Inventaris kompensasi tokokita:
| Step sukses | Kompensasi bila gagal |
|---|---|
| Reserve stock (product-service) | product.release → stok dikembalikan |
| Order CONFIRMED | order.cancelled |
| Payment PENDING | payment.cancel (void) |
| Payment SUCCESS | payment.refund (jarang; valid per policy) |
Contoh alur "gagal bayar" di choreography:
order.confirmed
→ payment-service charge → FAILED
payment.failed
→ order-service: CONFIRMED → CANCELLED (emit order.cancelled)
→ product-service: me-release stok (dari order.cancelled)
→ notification-service: kirim "pembayaran gagal, order dibatalkan"Keadaan akhirnya konsisten: order CANCELLED di DB order, stok kembali utuh di DB product, user ter-notifikasi. Tanpa transaksi bersama, semuanya terjadi sebagai rantai event — eventual consistency yang dalam jangka pendek masih bisa berbeda (stok belum turun?) tapi selamanya konvergen.
Order-service mencatat posisi saga dengan status + event log. Status sudah kita punya; tambahkan log langkah (saga step log) agar bisa di-audit dan direcovery:
create table saga_log (
id bigserial primary key,
order_id uuid not null,
step text not null, -- RESERVE_STOCK / CREATE_PAYMENT / ...
status text not null, -- STARTED / SUCCEEDED / COMPENSATED
created_at timestamptz not null default now()
);Dan timeout rules — bagian yang sering terlupakan. Order yang menunggu pembayaran terlalu lama (misal 30 menit) harus otomatis dibatalkan dan stok dilepas. Di Kafka, timeout bisa di-handle dengan event ber-schedule (episode 21 membahas timeline/watermarks) atau consumer yang memeriksa updated_at:
update orders
set status = 'CANCELLED', updated_at = now()
where status in ('CREATED','CONFIRMED')
and updated_at < now() - interval '30 minutes'
returning *;
— lalu emit order.cancelled untuk mengompensasi product-serviceWarning
Timeout itu bagian dari saga, bukan pelengkap. Tanpa timeout, order yang payment-nya gagal-senyap bisa menggantung di status CONFIRMED selamanya dengan stok terkunci. Tentukan SLO tiap step (reserve kurang dari 5 detik, payment kurang dari 30 menit) dan wajibkan penutup kompensasi — ini juga bahan SLI/SLO di episode 24.
Episode 13 menuntaskan persoalan konsistensi lintas layanan:
saga_log di order-service, timeout rules untuk menutup order menggantung.Di episode 14 selanjutnya, kita akan memecahkan masalah query lintas layanan dengan CQRS, read models, dan query pattern — memisahkan command dari query, membangun read model product yang sudah termasuk rating dan harga, API composition di gateway, serta catatan kapan event sourcing layak dipakai. Sampai jumpa di episode 14!