Tampilan
Kenapa penting
Di monolith, checkout cukup dibungkus satu @Transactional: tagih pembayaran, kurangi stok, buat pengiriman — gagal di tengah, semuanya di-rollback. Setelah data dipecah per service (bab sebelumnya), setiap service punya database sendiri dan tidak ada lagi transaksi yang membentang di semuanya. Kalau stok habis setelah uang pelanggan ditarik, siapa yang mengembalikan uangnya? Saga adalah jawaban standar industri untuk masalah ini, dan hampir pasti muncul di interview microservices maupun di sistem pembayaran, marketplace, atau logistik yang akan kamu kerjakan.
Konsep inti
Saga: urutan transaksi lokal + kompensasi
Saga adalah rangkaian transaksi lokal; tiap service meng-commit transaksinya sendiri lalu memicu langkah berikutnya. Bila satu langkah gagal, saga menjalankan compensating transaction untuk langkah-langkah yang sudah sukses, dalam urutan terbalik.
| Langkah | Service | Aksi | Kompensasi |
|---|---|---|---|
| 1 | order | buat pesanan PENDING | tandai CANCELLED |
| 2 | payment | tagih pembayaran | refund |
| 3 | inventory | reservasi stok | lepas reservasi |
| 4 | shipping | jadwalkan kurir | — (pivot, hanya maju) |
Kompensasi bukan rollback. Data sempat ter-commit dan mungkin sudah terlihat pihak lain (email "pembayaran diterima" bisa sudah terkirim). Kompensasi adalah transaksi bisnis baru yang membatalkan efeknya: refund, bukan "hapus baris pembayaran".
Langkah pivot
Langkah yang tidak bisa dikompensasi (mengirim barang, mencairkan dana ke bank) letakkan setelah semua langkah yang mungkin gagal. Setelah pivot, saga hanya maju — gagal berarti retry sampai berhasil.
Koreografi vs orkestrasi
Dua cara mengoordinasikan langkah saga:
| Aspek | Koreografi | Orkestrasi |
|---|---|---|
| Pengendali | tidak ada; tiap service bereaksi pada event | satu orkestrator memberi perintah |
| Alur terlihat di | tersebar di banyak consumer | satu class/workflow |
| Coupling | service saling tahu event satu sama lain | service hanya tahu orkestrator lewat perintah/balasan |
| Cocok untuk | 2–3 langkah sederhana | banyak langkah, banyak cabang, butuh status jelas |
| Risiko | siklus event, sulit di-debug | orkestrator jadi "god service" bila logika bisnis bocor ke sana |
Koreografi: OrderPlaced → payment menagih dan menerbitkan PaymentCompleted → inventory mereservasi dan menerbitkan StockReserved → shipping menjadwalkan. Orkestrasi: orkestrator mengirim ChargePayment, menunggu balasan, lalu mengirim ReserveStock, dan seterusnya.
Memodelkan state saga
Saga berjalan lama dan bisa terputus (service restart, broker lambat), jadi state-nya harus eksplisit dan tersimpan. Di Java, enum cocok untuk state, sealed interface + record untuk hasil langkah yang wajib ditangani semua cabangnya.
java
// Potongan
enum SagaState { STARTED, PAYMENT_CHARGED, STOCK_RESERVED, COMPLETED, COMPENSATING, FAILED }
sealed interface StepResult permits Ok, Failed {}
record Ok(String detail) implements StepResult {}
record Failed(String reason) implements StepResult {}Karena StepResult sealed, switch atas hasil langkah tidak butuh default — compiler memeriksa bahwa Ok dan Failed sama-sama ditangani. Di sistem nyata, state ini disimpan di tabel saga_instance (id saga, state, langkah terakhir, waktu update) agar orkestrator bisa melanjutkan setelah restart.
Idempotensi dan semantic lock
Broker seperti Kafka menjamin pengiriman at-least-once: pesan bisa datang dua kali. Karena itu setiap handler langkah dan kompensasi wajib idempoten — simpan kunci (misalnya orderId + nama langkah) beserta hasilnya; perintah yang sama datang lagi, kembalikan hasil lama tanpa menagih ulang.
Saga juga tidak punya isolation seperti ACID: pihak lain bisa melihat data "setengah jadi". Pola semantic lock menandai data dengan status sementara, misalnya PENDING_PAYMENT, lalu aturan bisnis menolak atau menunda operasi yang bentrok (pembatalan manual, edit alamat) sampai saga selesai.
Timeout, retry, dan observability
- Timeout: orkestrator menunggu balasan dengan batas waktu. Lewat batas, anggap gagal dan kompensasi — atau tanyakan status ke service target, karena "tidak ada balasan" belum tentu "tidak terjadi".
- Retry: ulangi kegagalan sementara (jaringan,
503) dengan backoff; kegagalan bisnis (stok habis, kartu ditolak) jangan di-retry, langsung kompensasi. - Observability: sertakan
sagaId/orderIddi setiap log, header pesan, dan trace. Sediakan query "saga yang macet di state X lebih dari 15 menit" — ini alarm terpenting di sistem saga.
Tool saga
Menulis orkestrator sendiri cocok untuk belajar dan alur sederhana. Untuk produksi dengan banyak langkah, tim sering memakai engine workflow: Temporal (workflow sebagai kode Java, state & retry dikelola engine), Camunda (BPMN, cocok bila alur perlu dibaca analis bisnis), atau Axon Framework (saga dalam ekosistem event sourcing). Prinsipnya tetap sama: langkah, kompensasi, idempotensi.
Contoh di dunia backend
Orkestrator checkout di bawah adalah simulasi Java murni — "service" berupa objek di memori, tanpa jaringan — agar alur kompensasi bisa kamu lihat langsung. Pembayaran sengaja dibuat idempoten berdasarkan orderId.
java
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class CheckoutSaga {
enum SagaState { STARTED, PAYMENT_CHARGED, STOCK_RESERVED, COMPLETED, COMPENSATING, FAILED }
record Order(String orderId, String sku, int qty, long amount) {}
sealed interface StepResult permits Ok, Failed {}
record Ok(String detail) implements StepResult {}
record Failed(String reason) implements StepResult {}
interface SagaStep {
String name();
StepResult execute(Order order);
void compensate(Order order);
}
// Simulasi layanan pembayaran: idempoten berdasarkan orderId.
static final class PaymentService implements SagaStep {
private final Set<String> charged = new HashSet<>();
public String name() { return "payment"; }
public StepResult execute(Order o) {
if (!charged.add(o.orderId())) return new Ok("sudah ditagih sebelumnya (idempoten)");
return new Ok("tagih Rp" + o.amount());
}
public void compensate(Order o) {
if (charged.remove(o.orderId())) System.out.println(" [kompensasi] payment: refund Rp" + o.amount());
}
}
static final class InventoryService implements SagaStep {
private int available;
InventoryService(int available) { this.available = available; }
public String name() { return "inventory"; }
public StepResult execute(Order o) {
if (available < o.qty()) return new Failed("stok " + o.sku() + " tinggal " + available);
available -= o.qty();
return new Ok("reservasi " + o.qty() + " x " + o.sku());
}
public void compensate(Order o) {
available += o.qty();
System.out.println(" [kompensasi] inventory: lepas reservasi " + o.qty() + " x " + o.sku());
}
}
static final class ShippingService implements SagaStep {
public String name() { return "shipping"; }
public StepResult execute(Order o) { return new Ok("jadwalkan kurir untuk " + o.orderId()); }
public void compensate(Order o) { System.out.println(" [kompensasi] shipping: batalkan kurir"); }
}
static SagaState run(Order order, List<SagaStep> steps) {
Deque<SagaStep> done = new ArrayDeque<>();
System.out.println("Saga " + order.orderId() + " dimulai");
for (SagaStep step : steps) {
StepResult result = step.execute(order);
switch (result) {
case Ok ok -> {
System.out.println(" " + step.name() + " OK: " + ok.detail());
done.push(step);
}
case Failed f -> {
System.out.println(" " + step.name() + " GAGAL: " + f.reason());
System.out.println(" state -> " + SagaState.COMPENSATING);
while (!done.isEmpty()) done.pop().compensate(order);
return SagaState.FAILED;
}
}
}
return SagaState.COMPLETED;
}
public static void main(String[] args) {
var payment = new PaymentService();
var inventory = new InventoryService(3);
var steps = List.<SagaStep>of(payment, inventory, new ShippingService());
var first = run(new Order("ORD-1", "KOPI-250G", 2, 150_000), steps);
System.out.println("Hasil: " + first);
var second = run(new Order("ORD-2", "KOPI-250G", 5, 375_000), steps);
System.out.println("Hasil: " + second);
}
}Jalankan dengan java CheckoutSaga.java:
text
Saga ORD-1 dimulai
payment OK: tagih Rp150000
inventory OK: reservasi 2 x KOPI-250G
shipping OK: jadwalkan kurir untuk ORD-1
Hasil: COMPLETED
Saga ORD-2 dimulai
payment OK: tagih Rp375000
inventory GAGAL: stok KOPI-250G tinggal 1
state -> COMPENSATING
[kompensasi] payment: refund Rp375000
Hasil: FAILEDPerhatikan tiga hal. Pertama, langkah sukses disimpan di Deque sebagai stack, sehingga kompensasi otomatis berjalan terbalik. Kedua, hanya langkah yang sudah sukses yang dikompensasi — inventory gagal, jadi tidak ada "lepas reservasi". Ketiga, versi produksi menyimpan SagaState ke database setiap transisi, mengirim perintah lewat broker (dengan outbox), dan menunggu balasan dengan timeout — kerangka logikanya tetap sama.
Pertanyaan interview
1. Kenapa tidak memakai two-phase commit (2PC) saja untuk transaksi lintas service?
2PC membutuhkan semua peserta mendukung protokol XA dan mengunci resource sampai koordinator memutuskan commit. Itu menurunkan availability (satu peserta lambat menahan semua), tidak didukung banyak broker dan database modern di cloud, serta meng-coupling service secara erat. Saga menukar isolation dengan availability: tiap langkah commit lokal, konsistensi dicapai secara eventual lewat kompensasi.
2. Kapan memilih orkestrasi dibanding koreografi?
Orkestrasi dipilih bila langkahnya banyak, ada percabangan, atau tim butuh jawaban cepat untuk "pesanan ini sedang di tahap apa?". Koreografi cukup untuk alur pendek dua-tiga langkah yang jarang berubah. Yang penting, logika bisnis tiap domain tetap di service-nya; orkestrator hanya mengurutkan.
3. Bagaimana menangani kompensasi yang gagal?
Kompensasi harus idempoten dan di-retry dengan backoff sampai berhasil, karena membiarkannya gagal berarti data permanen tidak konsisten. Bila tetap gagal setelah batas tertentu, saga masuk state khusus (misalnya COMPENSATION_FAILED), memicu alert, dan ditangani manual oleh tim operasional. Karena itu desain kompensasi sebaiknya sesederhana mungkin.
Jebakan umum
- Menganggap kompensasi sama dengan rollback, lalu "menghapus" data — padahal efeknya mungkin sudah terlihat pelanggan. Kompensasi adalah transaksi bisnis baru (refund, pembatalan).
- Handler langkah atau kompensasi tidak idempoten, sehingga pesan duplikat menagih atau me-refund dua kali.
- Meretry kegagalan bisnis seperti "kartu ditolak" — hasilnya tetap gagal, hanya menunda kompensasi.
- Menyimpan state saga hanya di memori; orkestrator restart dan saga yang sedang berjalan hilang tanpa jejak.
- Menaruh langkah yang tidak bisa dibatalkan di awal saga, sehingga kegagalan berikutnya tidak punya jalan keluar.
- Tidak mencatat
sagaIddi log dan trace, sehingga satu pesanan yang macet harus dicari di lima service secara manual.
Ringkasan
- Saga menggantikan transaksi ACID lintas service dengan rangkaian transaksi lokal plus compensating transaction yang berjalan terbalik.
- Koreografi mengandalkan event tanpa pengendali pusat; orkestrasi memusatkan alur dan state di satu komponen.
- Simpan state saga secara eksplisit (
enum+ tabel) dan modelkan hasil langkah dengansealed interface+record. - Pesan bisa datang dua kali — setiap langkah dan kompensasi wajib idempoten; pakai semantic lock untuk menutup celah isolation.
- Atur timeout dan retry hanya untuk kegagalan sementara, dan pasang
sagaIddi log serta trace agar saga yang macet mudah dilacak.