Saga Pattern Nedir?
Saga Pattern, mikroservis mimarilerinde dağıtık transaction'ları (distributed transactions) yönetmek için kullanılan bir tasarım desenidir. ACID'in aksine, Saga'lar "Eventually Consistent" (Nihai Tutarlılık) prensibiyle çalışır ve uzun süren işlemleri yönetir.
Saga Pattern vs ACID
| Özellik | ACID | Saga Pattern |
|---|---|---|
| Transaction tipi | Tek veritabanı, kısa süreli | Dağıtık, uzun süreli |
| Kilit mekanizması | Pessimistic locking | Optimistic, lock yok |
| Consistency | Strong consistency | Eventual consistency |
| Rollback | Otomatik | Compensating transactions |
| Performans | Düşük (kilitler nedeniyle) | Yüksek (asenkron) |
Saga Pattern Türleri
1. Choreography (Koreografi) - Event Tabanlı
Her servis kendi işini yapar ve event fırlatır. Diğer servisler bu eventleri dinler.
2. Orchestration (Orkestrasyon) - Merkezi Yönetim
Bir orkestratör (saga coordinator) tüm işlemleri yönetir ve sıralar.
Gerçek Hayat Örneği: Sipariş - Ödeme - Stok - Kargo
Senaryo: E-ticaret Sipariş Süreci
- Sipariş oluştur (Order Service)
- Ödemeyi al (Payment Service)
- Stoktan düş (Stock Service)
- Kargoya ver (Shipping Service)
1. Choreography (Koreografi) Tabanlı Saga
// Domain Events
public class OrderCreatedEvent {
private String orderId;
private String customerId;
private BigDecimal amount;
private List<OrderItem> items;
// getters, setters, constructor
}
public class PaymentProcessedEvent {
private String orderId;
private String paymentId;
private PaymentStatus status;
// getters, setters
}
public class StockReservedEvent {
private String orderId;
private List<ReservedItem> reservedItems;
private boolean success;
// getters, setters
}
public class ShippingScheduledEvent {
private String orderId;
private String trackingNumber;
private LocalDateTime estimatedDelivery;
// getters, setters
}
// Compensation Events
public class PaymentFailedEvent {
private String orderId;
private String reason;
// getters, setters
}
public class StockReservationFailedEvent {
private String orderId;
private String reason;
// getters, setters
}
// Order Service
@Service
public class OrderService {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
@Autowired
private OrderRepository orderRepository;
@Transactional
public Order createOrder(OrderRequest request) {
// 1. Siparişi kaydet (PENDING durumunda)
Order order = new Order();
order.setCustomerId(request.getCustomerId());
order.setItems(request.getItems());
order.setTotalAmount(request.getTotalAmount());
order.setStatus(OrderStatus.PENDING);
order = orderRepository.save(order);
// 2. Event fırlat - diğer servisler dinleyecek
OrderCreatedEvent event = new OrderCreatedEvent();
event.setOrderId(order.getId());
event.setCustomerId(order.getCustomerId());
event.setAmount(order.getTotalAmount());
event.setItems(order.getItems());
kafkaTemplate.send("order-created-topic", event);
return order;
}
// Compensation - Payment Failed handler
@KafkaListener(topics = "payment-failed-topic")
public void handlePaymentFailed(PaymentFailedEvent event) {
Order order = orderRepository.findById(event.getOrderId()).get();
order.setStatus(OrderStatus.PAYMENT_FAILED);
orderRepository.save(order);
// İptal edildi bilgilendirmesi
notifyCustomer(order.getCustomerId(), "Ödeme başarısız, sipariş iptal edildi");
}
}
// Payment Service
@Service
public class PaymentService {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
@Autowired
private PaymentRepository paymentRepository;
@KafkaListener(topics = "order-created-topic")
public void handleOrderCreated(OrderCreatedEvent event) {
try {
// Ödeme işlemini gerçekleştir
Payment payment = processPayment(event);
// Başarılı event fırlat
PaymentProcessedEvent successEvent = new PaymentProcessedEvent();
successEvent.setOrderId(event.getOrderId());
successEvent.setPaymentId(payment.getId());
successEvent.setStatus(PaymentStatus.SUCCESS);
kafkaTemplate.send("payment-processed-topic", successEvent);
} catch (Exception e) {
// Başarısız event fırlat (compensation)
PaymentFailedEvent failedEvent = new PaymentFailedEvent();
failedEvent.setOrderId(event.getOrderId());
failedEvent.setReason(e.getMessage());
kafkaTemplate.send("payment-failed-topic", failedEvent);
}
}
private Payment processPayment(OrderCreatedEvent event) {
// Ödeme gateway entegrasyonu
Payment payment = new Payment();
payment.setOrderId(event.getOrderId());
payment.setAmount(event.getAmount());
payment.setStatus(PaymentStatus.PROCESSING);
payment = paymentRepository.save(payment);
// Ödeme işlemi simülasyonu
if (event.getAmount().compareTo(new BigDecimal("10000")) > 0) {
throw new RuntimeException("Yetersiz bakiye");
}
payment.setStatus(PaymentStatus.COMPLETED);
payment.setPaymentDate(LocalDateTime.now());
return paymentRepository.save(payment);
}
}
// Stock Service
@Service
public class StockService {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
@Autowired
private StockRepository stockRepository;
@KafkaListener(topics = "payment-processed-topic")
public void handlePaymentProcessed(PaymentProcessedEvent event) {
try {
// Stok rezervasyonu yap
reserveStock(event);
// Başarılı event fırlat
StockReservedEvent successEvent = new StockReservedEvent();
successEvent.setOrderId(event.getOrderId());
successEvent.setSuccess(true);
kafkaTemplate.send("stock-reserved-topic", successEvent);
} catch (Exception e) {
// Başarısız event fırlat (compensation)
StockReservationFailedEvent failedEvent = new StockReservationFailedEvent();
failedEvent.setOrderId(event.getOrderId());
failedEvent.setReason(e.getMessage());
kafkaTemplate.send("stock-failed-topic", failedEvent);
}
}
private void reserveStock(PaymentProcessedEvent event) {
// Stok kontrolü ve rezervasyon
// Örnek: Her ürün için stok kontrolü
List<OrderItem> items = getOrderItems(event.getOrderId());
for (OrderItem item : items) {
Stock stock = stockRepository.findByProductId(item.getProductId());
if (stock.getQuantity() < item.getQuantity()) {
throw new RuntimeException("Yetersiz stok: " + item.getProductId());
}
stock.setQuantity(stock.getQuantity() - item.getQuantity());
stockRepository.save(stock);
}
}
// Compensation - Payment Failed handler
@KafkaListener(topics = "payment-failed-topic")
public void handlePaymentFailed(PaymentFailedEvent event) {
// Stok rezervasyonu yapılmadı, bir şey yapmaya gerek yok
log.info("Payment failed for order: {}, no stock reservation needed", event.getOrderId());
}
}
// Shipping Service
@Service
public class ShippingService {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
@Autowired
private ShippingRepository shippingRepository;
@KafkaListener(topics = "stock-reserved-topic")
public void handleStockReserved(StockReservedEvent event) {
try {
// Kargo planlaması yap
Shipping shipping = scheduleShipping(event);
// Başarılı event fırlat
ShippingScheduledEvent successEvent = new ShippingScheduledEvent();
successEvent.setOrderId(event.getOrderId());
successEvent.setTrackingNumber(shipping.getTrackingNumber());
successEvent.setEstimatedDelivery(shipping.getEstimatedDelivery());
kafkaTemplate.send("shipping-scheduled-topic", successEvent);
// Order Service'i güncelle
updateOrderStatus(event.getOrderId(), OrderStatus.COMPLETED);
} catch (Exception e) {
// Kargo planlaması başarısız - compensation başlat
// Stock'u geri al, ödemeyi iade et
initiateCompensation(event.getOrderId(), e.getMessage());
}
}
private Shipping scheduleShipping(StockReservedEvent event) {
Shipping shipping = new Shipping();
shipping.setOrderId(event.getOrderId());
shipping.setStatus(ShippingStatus.PLANNED);
shipping.setEstimatedDelivery(LocalDateTime.now().plusDays(3));
shipping.setTrackingNumber("TRK" + UUID.randomUUID().toString());
return shippingRepository.save(shipping);
}
private void initiateCompensation(String orderId, String reason) {
// 1. Stok geri al
kafkaTemplate.send("stock-revert-topic", orderId);
// 2. Ödemeyi iade et
kafkaTemplate.send("payment-refund-topic", orderId);
// 3. Siparişi iptal et
kafkaTemplate.send("order-cancel-topic", orderId);
}
}
2. Orchestration (Orkestrasyon) Tabanlı Saga
// Saga Orchestrator
@Component
public class OrderSagaOrchestrator {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
@Autowired
private SagaStateRepository sagaStateRepository;
@Autowired
private RestTemplate restTemplate;
public void startSaga(OrderCreatedEvent event) {
// Saga state'i kaydet
SagaState state = new SagaState();
state.setSagaId(UUID.randomUUID().toString());
state.setOrderId(event.getOrderId());
state.setCurrentStep(SagaStep.CREATE_ORDER);
state.setStatus(SagaStatus.STARTED);
sagaStateRepository.save(state);
// İlk adım: Ödeme işle
processPayment(state);
}
private void processPayment(SagaState state) {
try {
// Payment Service'e REST çağrısı
PaymentRequest request = new PaymentRequest();
request.setOrderId(state.getOrderId());
ResponseEntity<PaymentResponse> response = restTemplate.postForEntity(
"http://payment-service/api/payments",
request,
PaymentResponse.class
);
if (response.getBody().isSuccess()) {
// Başarılı - Stok adımına geç
state.setCurrentStep(SagaStep.PROCESS_PAYMENT);
sagaStateRepository.save(state);
reserveStock(state, response.getBody().getPaymentId());
} else {
// Başarısız - Saga'yı bitir
failSaga(state, "Ödeme başarısız");
}
} catch (Exception e) {
// Hata durumunda compensation başlat
compensateSaga(state, "Ödeme hatası: " + e.getMessage());
}
}
private void reserveStock(SagaState state, String paymentId) {
try {
// Stock Service'e REST çağrısı
StockReservationRequest request = new StockReservationRequest();
request.setOrderId(state.getOrderId());
ResponseEntity<StockReservationResponse> response = restTemplate.postForEntity(
"http://stock-service/api/stock/reserve",
request,
StockReservationResponse.class
);
if (response.getBody().isSuccess()) {
// Başarılı - Kargo adımına geç
state.setCurrentStep(SagaStep.RESERVE_STOCK);
sagaStateRepository.save(state);
scheduleShipping(state, paymentId, response.getBody().getReservationId());
} else {
// Stok yok - Ödemeyi iade et
compensateSaga(state, "Stok yetersiz");
}
} catch (Exception e) {
compensateSaga(state, "Stok rezervasyon hatası: " + e.getMessage());
}
}
private void scheduleShipping(SagaState state, String paymentId, String reservationId) {
try {
// Shipping Service'e REST çağrısı
ShippingRequest request = new ShippingRequest();
request.setOrderId(state.getOrderId());
ResponseEntity<ShippingResponse> response = restTemplate.postForEntity(
"http://shipping-service/api/shipping/schedule",
request,
ShippingResponse.class
);
if (response.getBody().isSuccess()) {
// Başarılı - Saga tamamlandı
state.setCurrentStep(SagaStep.SCHEDULE_SHIPPING);
state.setStatus(SagaStatus.COMPLETED);
sagaStateRepository.save(state);
// Order Service'i bilgilendir
completeOrder(state.getOrderId(), response.getBody().getTrackingNumber());
} else {
// Kargo planlaması başarısız - Compensation
compensateSaga(state, "Kargo planlaması başarısız");
}
} catch (Exception e) {
compensateSaga(state, "Kargo planlaması hatası: " + e.getMessage());
}
}
private void compensateSaga(SagaState state, String reason) {
log.error("Saga compensation başlatılıyor: {} - Reason: {}", state.getSagaId(), reason);
// Hangi adımdaysak ona göre compensation
switch (state.getCurrentStep()) {
case PROCESS_PAYMENT:
// Sadece ödeme yapıldıysa iade et
refundPayment(state.getOrderId());
break;
case RESERVE_STOCK:
// Stok rezervasyonu varsa geri al ve ödemeyi iade et
revertStock(state.getOrderId());
refundPayment(state.getOrderId());
break;
case SCHEDULE_SHIPPING:
// Kargo planlandıysa iptal et, stoku geri al, ödemeyi iade et
cancelShipping(state.getOrderId());
revertStock(state.getOrderId());
refundPayment(state.getOrderId());
break;
}
// Saga state'i güncelle
state.setStatus(SagaStatus.COMPENSATED);
state.setErrorMessage(reason);
sagaStateRepository.save(state);
// Order Service'i bilgilendir
failOrder(state.getOrderId(), reason);
}
// Compensation metotları
private void refundPayment(String orderId) {
kafkaTemplate.send("payment-refund-topic", orderId);
}
private void revertStock(String orderId) {
kafkaTemplate.send("stock-revert-topic", orderId);
}
private void cancelShipping(String orderId) {
kafkaTemplate.send("shipping-cancel-topic", orderId);
}
private void completeOrder(String orderId, String trackingNumber) {
OrderCompletedEvent event = new OrderCompletedEvent();
event.setOrderId(orderId);
event.setTrackingNumber(trackingNumber);
event.setStatus("COMPLETED");
kafkaTemplate.send("order-completed-topic", event);
}
private void failOrder(String orderId, String reason) {
OrderFailedEvent event = new OrderFailedEvent();
event.setOrderId(orderId);
event.setReason(reason);
kafkaTemplate.send("order-failed-topic", event);
}
}
// Saga State Entity
@Entity
public class SagaState {
@Id
private String sagaId;
private String orderId;
@Enumerated(EnumType.STRING)
private SagaStep currentStep;
@Enumerated(EnumType.STRING)
private SagaStatus status;
private String errorMessage;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// getters, setters
}
public enum SagaStep {
CREATE_ORDER,
PROCESS_PAYMENT,
RESERVE_STOCK,
SCHEDULE_SHIPPING
}
public enum SagaStatus {
STARTED,
COMPLETED,
FAILED,
COMPENSATED
}
Önemli Saga Pattern Konuları
1. Idempotency (Tekrarlanabilirlik)
@Service
public class IdempotentPaymentService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public PaymentResult processPayment(PaymentRequest request) {
// Idempotency key kontrolü
String idempotencyKey = request.getIdempotencyKey();
String processed = redisTemplate.opsForValue().get(idempotencyKey);
if (processed != null) {
// Aynı istek daha önce işlendi
return getCachedResult(idempotencyKey);
}
// İşlemi gerçekleştir
PaymentResult result = doProcessPayment(request);
// Sonucu cache'le (TTL ile)
redisTemplate.opsForValue().set(idempotencyKey,
result.getPaymentId(),
Duration.ofHours(24));
return result;
}
}
2. Compensation Transaction Örnekleri
@Service
public class CompensationManager {
@Transactional
public void compensateOrder(String orderId) {
// 1. Adım: Stok geri al
compensateStock(orderId);
// 2. Adım: Ödemeyi iade et
compensatePayment(orderId);
// 3. Adım: Sipariş durumunu güncelle
updateOrderStatus(orderId, OrderStatus.CANCELLED);
// 4. Adım: Müşteriyi bilgilendir
notifyCustomer(orderId, "Siparişiniz iptal edildi");
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void compensateStock(String orderId) {
try {
// Stok servisine compensation çağrısı
stockService.revertStock(orderId);
} catch (Exception e) {
// Manual intervention gerekebilir
log.error("Stock compensation failed for order: {}", orderId);
alertTeam("Stock compensation failed", orderId);
}
}
}
3. Saga Monitoring ve Recovery
@Component
public class SagaMonitor {
@Scheduled(fixedDelay = 60000) // Her dakika
public void checkStuckSagas() {
// 10 dakikadan uzun süredir devam eden sagaları bul
List<SagaState> stuckSagas = sagaStateRepository
.findByStatusAndLastUpdatedBefore(
SagaStatus.STARTED,
LocalDateTime.now().minusMinutes(10)
);
for (SagaState saga : stuckSagas) {
// Manual intervention veya otomatik recovery
recoverSaga(saga);
}
}
private void recoverSaga(SagaState saga) {
// Saga'yı yeniden başlat veya compensation uygula
if (saga.getCurrentStep() == SagaStep.PROCESS_PAYMENT) {
// Ödeme adımında takıldıysa, payment service'i kontrol et
PaymentStatus status = checkPaymentStatus(saga.getOrderId());
if (status == PaymentStatus.SUCCESS) {
// Ödeme başarılıysa devam et
orchestrator.reserveStock(saga);
} else {
// Ödeme başarısızsa compensation başlat
orchestrator.compensateSaga(saga, "Timeout - Payment stuck");
}
}
}
}
Saga Pattern Best Practices
1. Event Sıralaması ve Correlation
public class SagaEvent {
private String sagaId; // Hangi saga'ya ait
private String correlationId; // Hangi işleme ait
private String eventType; // Event tipi
private String step; // Hangi adım
private Object data; // Event verisi
private LocalDateTime timestamp;
// getters, setters
}
2. Timeout ve Retry Mekanizması
@Configuration
public class SagaRetryConfig {
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
// 3 kez dene, her denemede bekleme süresini artır
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000);
backOffPolicy.setMultiplier(2);
backOffPolicy.setMaxInterval(10000);
retryTemplate.setBackOffPolicy(backOffPolicy);
// Sadece belirli hatalarda retry yap
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3);
retryTemplate.setRetryPolicy(retryPolicy);
return retryTemplate;
}
}
3. Dead Letter Queue (DLQ) Kullanımı
@Component
public class SagaDeadLetterHandler {
@KafkaListener(topics = "saga-dlq-topic")
public void handleDeadLetter(SagaDeadLetterMessage message) {
log.error("Dead letter alındı: {}", message);
// Manual intervention için alert gönder
alertTeam("Saga DLQ message received", message);
// DLQ'daki mesajı veritabanına kaydet
saveToDeadLetterTable(message);
// Gerekirse otomatik recovery dene
if (message.getRetryCount() < 3) {
retryMessage(message);
}
}
}
Saga Pattern Ne Zaman Kullanılmalı?
Kullanılması Gereken Durumlar:
- Mikroservisler arası işlemler: Birden fazla servisi güncelleyen işlemler
- Uzun süren işlemler: Saatler/günler sürebilen business process'ler
- Yüksek skalabilite gereken durumlar: Lock mekanizması istemeyen sistemler
- Event-driven mimariler: Asenkron iletişimin olduğu sistemler
Kullanılmaması Gereken Durumlar:
- Tek veritabanı işlemleri: ACID yeterli ve daha basit
- Kısa süreli kritik işlemler: Finansal işlemlerde ACID daha güvenli
- Basit CRUD operasyonları: Ekstra complexity gereksiz
Saga Pattern, dağıtık sistemlerde veri tutarlılığını sağlamak için güçlü bir araçtır ancak getirdiği complexity'nin farkında olmak ve doğru durumlarda kullanmak önemlidir.