Saga Pattern
Microservice mimarisinde her servisin kendi veritabanı vardır. Ancak bir iş akışı (örneğin, sipariş oluşturma) birden fazla servisi kapsadığında, dağıtık transaction problemi ortaya çıkar. Geleneksel ACID transaction'ları tek bir veritabanında çalışır — birden fazla servisteki veritabanlarını kapsayan "global transaction" hem zor hem de performans açısından pahalıdır. Saga pattern, bu problemi çözmek için tasarlanmış bir dağıtık işlem yönetimi kalıbıdır.
Dağıtık Transaction Problemi
Monolitik mimaride tek bir veritabanı transaction'ı içinde tüm işlemleri atomik olarak yapabilirsiniz:
// Monolith — tek transaction
@Transactional
public void createOrder(OrderRequest request) {
orderRepository.save(order); // 1. Sipariş oluştur
inventoryService.reserve(items); // 2. Stok ayır
paymentService.charge(payment); // 3. Ödeme al
// Hepsi aynı DB transaction'ında — ya hep ya hiç
}Microservice mimarisinde bu mümkün değildir çünkü:
Her servisin kendi veritabanı vardır (Database per Service pattern)
Servisler arası iletişim ağ üzerinden yapılır — ağ güvenilir değildir
Two-Phase Commit (2PC) çözümü vardır ancak performans düşüklüğü, single point of failure ve tüm katılımcıların aynı anda erişilebilir olması gerekliliği nedeniyle microservice dünyasında kaçınılır.
Saga Nedir?
Saga, birden fazla servisi kapsayan bir iş akışını, her biri tek bir serviste çalışan yerel transaction'lar dizisi olarak yöneten bir pattern'dır. Her yerel transaction, veritabanını günceller ve bir sonraki adımı tetiklemek için event veya komut yayınlar. Herhangi bir adım başarısız olursa, önceki adımların etkisini geri alan compensating transaction'lar çalıştırılır.
Sipariş Saga — Başarılı Akış:
T1: Order Service → Sipariş oluştur (PENDING)
T2: Inventory Service → Stok ayır
T3: Payment Service → Ödeme al
T4: Order Service → Sipariş onayla (CONFIRMED)
Sipariş Saga — Başarısız Akış (T3 hata):
T1: Order Service → Sipariş oluştur (PENDING)
T2: Inventory Service → Stok ayır
T3: Payment Service → Ödeme BAŞARISIZ ✗
C2: Inventory Service → Stok ayırmayı geri al (compensate)
C1: Order Service → Siparişi iptal et (CANCELLED)Choreography Saga (Koreografi)
Choreography modelinde merkezi bir koordinatör yoktur. Her servis, kendi işini tamamladıktan sonra bir event yayınlar; diğer servisler bu event'i dinleyerek kendi adımlarını gerçekleştirir.
Order Service Inventory Service Payment Service
│ │ │
│── OrderCreated ──▶ │ │
│ InventoryReserved ──▶ │
│ │ PaymentCompleted
│◀───────────────────────────── │ ◀───────────────────────│
│ (OrderConfirmed) │ │// Order Service — Saga başlangıcı
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final KafkaTemplate<String, Object> kafka;
@Transactional
public Order createOrder(OrderRequest request) {
Order order = Order.builder()
.status(OrderStatus.PENDING)
.items(request.getItems())
.totalAmount(request.getTotalAmount())
.build();
orderRepository.save(order);
// Event yayınla — Inventory Service dinleyecek
kafka.send("order-events", order.getId(),
new OrderCreatedEvent(order.getId(), order.getItems()));
return order;
}
// Compensation — Ödeme başarısız olursa
@KafkaListener(topics = "payment-events", groupId = "order-service")
public void handlePaymentFailed(PaymentFailedEvent event) {
Order order = orderRepository.findById(event.getOrderId()).orElseThrow();
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
}
// Inventory Service — Stok ayırma
@Service
@RequiredArgsConstructor
public class InventoryConsumer {
private final InventoryService inventoryService;
private final KafkaTemplate<String, Object> kafka;
@KafkaListener(topics = "order-events", groupId = "inventory-service")
public void handleOrderCreated(OrderCreatedEvent event) {
try {
inventoryService.reserveStock(event.getOrderId(), event.getItems());
kafka.send("inventory-events", event.getOrderId(),
new InventoryReservedEvent(event.getOrderId()));
} catch (InsufficientStockException e) {
kafka.send("inventory-events", event.getOrderId(),
new InventoryReservationFailedEvent(event.getOrderId(), e.getMessage()));
}
}
// Compensation — Ödeme başarısızsa stok geri al
@KafkaListener(topics = "payment-events", groupId = "inventory-service")
public void handlePaymentFailed(PaymentFailedEvent event) {
inventoryService.releaseStock(event.getOrderId());
}
}Choreography avantajları: Basit, ek servis gerekmez, servisler gevşek bağlı.
Choreography dezavantajları: Akış izlenmesi zor (distributed tracing gerekir), döngüsel bağımlılıklar oluşabilir, yeni adım eklemek zor — servis sayısı arttıkça karmaşıklık katlanarak artar.
Orchestration Saga (Orkestrasyon)
Orchestration modelinde merkezi bir Saga Orchestrator (koordinatör) tüm akışı yönetir. Orchestrator, her servise komut gönderir ve sonuçlara göre bir sonraki adıma veya compensating transaction'a karar verir.
Saga Orchestrator
│
┌────────────────┼────────────────┐
▼ ▼ ▼
Order Service Inventory Service Payment Service
(create/cancel) (reserve/release) (charge/refund)// Saga Orchestrator
@Service
@RequiredArgsConstructor
@Slf4j
public class CreateOrderSaga {
private final OrderService orderService;
private final KafkaTemplate<String, Object> kafka;
private final SagaStateRepository sagaRepository;
public void start(OrderRequest request) {
// Saga durumunu oluştur
SagaState saga = SagaState.builder()
.sagaId(UUID.randomUUID().toString())
.status(SagaStatus.STARTED)
.data(objectMapper.writeValueAsString(request))
.currentStep("CREATE_ORDER")
.build();
sagaRepository.save(saga);
// Adım 1: Sipariş oluştur
Order order = orderService.createOrder(request);
saga.setOrderId(order.getId());
saga.setCurrentStep("RESERVE_INVENTORY");
sagaRepository.save(saga);
// Adım 2: Stok ayırma komutu gönder
kafka.send("inventory-commands", new ReserveStockCommand(
saga.getSagaId(), order.getId(), request.getItems()));
}
@KafkaListener(topics = "saga-replies", groupId = "saga-orchestrator")
public void handleReply(SagaReply reply) {
SagaState saga = sagaRepository.findById(reply.getSagaId()).orElseThrow();
switch (saga.getCurrentStep()) {
case "RESERVE_INVENTORY" -> {
if (reply.isSuccess()) {
saga.setCurrentStep("PROCESS_PAYMENT");
sagaRepository.save(saga);
kafka.send("payment-commands",
new ChargePaymentCommand(saga.getSagaId(),
saga.getOrderId(), saga.getTotalAmount()));
} else {
compensate(saga, "INVENTORY_FAILED");
}
}
case "PROCESS_PAYMENT" -> {
if (reply.isSuccess()) {
saga.setCurrentStep("COMPLETED");
saga.setStatus(SagaStatus.COMPLETED);
sagaRepository.save(saga);
orderService.confirmOrder(saga.getOrderId());
} else {
compensate(saga, "PAYMENT_FAILED");
}
}
}
}
private void compensate(SagaState saga, String reason) {
log.warn("Saga {} compensating: {}", saga.getSagaId(), reason);
saga.setStatus(SagaStatus.COMPENSATING);
sagaRepository.save(saga);
// Geriye doğru compensating işlemler
switch (reason) {
case "PAYMENT_FAILED" -> {
kafka.send("inventory-commands",
new ReleaseStockCommand(saga.getSagaId(), saga.getOrderId()));
orderService.cancelOrder(saga.getOrderId());
}
case "INVENTORY_FAILED" -> {
orderService.cancelOrder(saga.getOrderId());
}
}
saga.setStatus(SagaStatus.COMPENSATED);
sagaRepository.save(saga);
}
}Orchestration avantajları: Akış tek noktadan izlenir, karmaşık senaryolar kolay yönetilir, döngüsel bağımlılık riski yoktur, yeni adım eklemek kolaydır.
Orchestration dezavantajları: Orchestrator ek bir servis/bileşendir (bakım gerektirir), tek nokta arızası (single point of failure) riski — HA yapılandırması gerekir.
Compensating Transactions
Compensating transaction, bir saga adımının etkisini semantik olarak geri alan işlemdir. Dikkat: bu bir veritabanı rollback'i değildir — iş mantığı seviyesinde geri almadır.
| Orijinal İşlem | Compensating Transaction |
|---|---|
| Sipariş oluştur | Siparişi iptal et |
| Stok ayır | Stok ayırmayı geri al |
| Ödeme al | İade (refund) yap |
| E-posta gönder | İptal e-postası gönder |
| Kargo başlat | Kargo iptal talebi oluştur |
Önemli kurallar:
Compensating transaction idempotent olmalıdır (tekrar çağrılabilir).
Compensating transaction başarısız olmadan çalışabilmelidir — eğer compensation da başarısız olursa, insan müdahalesi gerekir.
Bazı işlemler geri alınamaz (iade süresi geçmiş ödeme, gönderilmiş e-posta). Bu durumlarda alternatif stratejiler gerekir.
Saga State Machine
Saga'nın durumunu yönetmenin en güvenilir yolu state machine kullanmaktır. Her saga durumu (state) ve geçişi (transition) açıkça tanımlanır:
public enum OrderSagaState {
STARTED,
INVENTORY_RESERVED,
PAYMENT_PROCESSED,
COMPLETED,
INVENTORY_RESERVATION_FAILED,
PAYMENT_FAILED,
COMPENSATING,
COMPENSATED
}STARTED ──(success)──▶ INVENTORY_RESERVED ──(success)──▶ PAYMENT_PROCESSED ──▶ COMPLETED
│ │ │
│ (fail) │ (fail) │ (fail)
▼ ▼ ▼
COMPENSATED ◀──────── COMPENSATING ◀───────────────── COMPENSATINGSpring State Machine veya basit bir enum-based durum yönetimi ile saga durumları takip edilebilir. State machine yaklaşımı, olası tüm durumları ve geçişleri açıkça tanımlayarak beklenmedik durumları önler.
Choreography vs Orchestration: Ne Zaman Hangisi?
| Kriter | Choreography | Orchestration |
|---|---|---|
| Servis sayısı | 2-4 servis | 4+ servis |
| Akış karmaşıklığı | Basit, lineer | Koşullu, paralel adımlar |
| Bağımlılık | Servisler event'leri bilir | Sadece orchestrator bilir |
| İzlenebilirlik | Zor (distributed tracing) | Kolay (merkezi state) |
| Yeni adım ekleme | Tüm servisler etkilenebilir | Sadece orchestrator güncellenir |
| Hata yönetimi | Her servis kendi compensation'ını yapar | Orchestrator yönetir |
AI Asistan
Sorularını yanıtlamaya hazır