Dlaczego Saga Pattern jest niezbędny w mikrousługach
W świecie mikrousług tradycyjne transakcje ACID stają się niemożliwe do zastosowania. Gdy operacja biznesowa wymaga zmian w wielu serwisach (każdy z własną bazą danych), nie możemy już polegać na dwufazowym commicie (2PC), który jest zbyt powolny i zawodny w środowisku rozproszonym.
Problem ilustruje klasyczny przykład e-commerce: zamówienie wymaga weryfikacji płatności, rezerwacji towaru w magazynie i zaktualizowania konta klienta. W architekturze mikrousług to oznacza komunikację między trzema niezależnymi serwisami.
Co się nauczysz:
- Jak implementować Saga pattern w Spring Boot aplikacjach
- Różnice między Choreography i Orchestration approach
- Praktyczne wzorce kompensacji i error handling
- Performance i consistency trade-offs
- Production-ready monitoring i debugging saga flows
- Integration z Apache Kafka i Spring Cloud
Dwa podejścia do implementacji Saga
Choreography-based Saga
W tym podejściu każdy serwis zna tylko swój następny krok. Komunikacja odbywa się przez events, a każdy serwis nasłuchuje określonych zdarzeń i odpowiada publikowaniem własnych.
@Service @Component public class OrderService { @Autowired private EventPublisher eventPublisher; @Autowired private OrderRepository orderRepository; public void createOrder(CreateOrderRequest request) { // 1. Tworzymy zamówienie w stanie PENDING Order order = new Order(request.getUserId(), request.getItems(), OrderStatus.PENDING); orderRepository.save(order); // 2. Publikujemy event do Payment Service PaymentRequested event = new PaymentRequested( order.getId(), order.getTotalAmount(), order.getUserId() ); eventPublisher.publish("payment.requested", event); } @EventListener public void handlePaymentProcessed(PaymentProcessedEvent event) { Order order = orderRepository.findById(event.getOrderId()); order.markPaymentCompleted(); orderRepository.save(order); // Następny krok - rezerwacja w magazynie InventoryReservationRequested inventoryEvent = new InventoryReservationRequested( order.getId(), order.getItems() ); eventPublisher.publish("inventory.reservation.requested", inventoryEvent); } @EventListener public void handleInventoryReserved(InventoryReservedEvent event) { Order order = orderRepository.findById(event.getOrderId()); order.markInventoryReserved(); order.setStatus(OrderStatus.CONFIRMED); orderRepository.save(order); // Saga completed successfully OrderConfirmedEvent confirmedEvent = new OrderConfirmedEvent(order.getId()); eventPublisher.publish("order.confirmed", confirmedEvent); } }
Orchestration-based Saga
Tutaj jeden centralny komponent (orchestrator) zarządza całym procesem, wywołując poszczególne serwisy i reagując na ich odpowiedzi.
@Service public class OrderSagaOrchestrator { @Autowired private PaymentServiceClient paymentService; @Autowired private InventoryServiceClient inventoryService; @Autowired private OrderService orderService; public void processOrder(CreateOrderRequest request) { SagaTransaction saga = new SagaTransaction(request); try { // Step 1: Create order Order order = orderService.createOrder(request); saga.addCompensation(() -> orderService.cancelOrder(order.getId())); // Step 2: Process payment PaymentResult payment = paymentService.processPayment( order.getTotalAmount(), request.getPaymentDetails() ); saga.addCompensation(() -> paymentService.refundPayment(payment.getId())); // Step 3: Reserve inventory InventoryReservation reservation = inventoryService.reserveItems( order.getItems() ); saga.addCompensation(() -> inventoryService.releaseReservation(reservation.getId())); // Step 4: Confirm order orderService.confirmOrder(order.getId()); saga.markCompleted(); } catch (Exception e) { // Coś poszło nie tak - uruchamiamy kompensację saga.compensate(); throw new OrderProcessingException("Failed to process order", e); } } }
Implementacja mechanizmów kompensacji
Kluczowym elementem Saga pattern są compensating actions – operacje, które „cofają” skutki wcześniejszych kroków.
public class SagaTransaction { private Listcompensations = new ArrayList<>(); private boolean completed = false; public void addCompensation(CompensationAction action) { compensations.add(action); } public void compensate() { // Wykonujemy kompensacje w odwrotnej kolejności Collections.reverse(compensations); for (CompensationAction action : compensations) { try { action.execute(); } catch (Exception e) { // Logujemy błąd ale kontynuujemy - // nie możemy zatrzymać procesu kompensacji log.error("Compensation failed", e); } } } @FunctionalInterface public interface CompensationAction { void execute() throws Exception; } } @Service public class PaymentService { public PaymentResult processPayment(BigDecimal amount, PaymentDetails details) { // Przetwarzanie płatności PaymentResult result = chargeCard(details.getCardNumber(), amount); return result; } // Compensation action public void refundPayment(String paymentId) { Payment payment = paymentRepository.findById(paymentId); if (payment.getStatus() == PaymentStatus.COMPLETED) { // Wykonujemy refund refundProcessor.processRefund(payment); payment.setStatus(PaymentStatus.REFUNDED); paymentRepository.save(payment); } } }
Event sourcing w Saga pattern
Połączenie Saga pattern z Event Sourcing daje potężne możliwości auditingu i recovery:
@Entity public class SagaEvent { @Id private String id; private String sagaId; private String eventType; private String eventData; private LocalDateTime timestamp; private SagaEventStatus status; // getters, setters } @Service public class SagaEventStore { @Autowired private SagaEventRepository repository; public void recordEvent(String sagaId, String eventType, Object eventData) { SagaEvent event = new SagaEvent(); event.setId(UUID.randomUUID().toString()); event.setSagaId(sagaId); event.setEventType(eventType); event.setEventData(objectMapper.writeValueAsString(eventData)); event.setTimestamp(LocalDateTime.now()); event.setStatus(SagaEventStatus.PENDING); repository.save(event); } public ListgetSagaHistory(String sagaId) { return repository.findBySagaIdOrderByTimestamp(sagaId); } public void markEventCompleted(String eventId) { SagaEvent event = repository.findById(eventId); event.setStatus(SagaEventStatus.COMPLETED); repository.save(event); } }
Integration z Apache Kafka
W produkcji często używamy Apache Kafka jako event backbone dla Saga pattern:
@Configuration @EnableKafka public class KafkaConfig { @Bean public ProducerFactoryproducerFactory() { Map configProps = new HashMap<>(); configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); configProps.put(ProducerConfig.ACKS_CONFIG, "all"); // Ensure durability return new DefaultKafkaProducerFactory<>(configProps); } @Bean public KafkaTemplate kafkaTemplate() { return new KafkaTemplate<>(producerFactory()); } } @Service public class KafkaSagaEventPublisher { @Autowired private KafkaTemplate kafkaTemplate; public void publishSagaEvent(String sagaId, SagaEvent event) { // Używamy sagaId jako partition key dla ordered processing kafkaTemplate.send("saga-events", sagaId, event) .addCallback( result -> log.info("Event published successfully"), failure -> log.error("Failed to publish event", failure) ); } } @Component public class SagaEventListener { @KafkaListener(topics = "payment-events", groupId = "order-saga-group") public void handlePaymentEvent(PaymentEvent event) { if (event.getType() == PaymentEventType.PAYMENT_COMPLETED) { // Kontynuuj saga - następny krok processNextSagaStep(event.getSagaId(), event); } else if (event.getType() == PaymentEventType.PAYMENT_FAILED) { // Uruchom kompensację initiateSagaCompensation(event.getSagaId(), event); } } }
Monitoring i observability
Production-ready saga implementation wymaga comprehensive monitoring:
@Component public class SagaMetrics { private final MeterRegistry meterRegistry; private final Counter sagaStartedCounter; private final Counter sagaCompletedCounter; private final Counter sagaFailedCounter; private final Timer sagaDurationTimer; public SagaMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.sagaStartedCounter = Counter.builder("saga.started") .description("Number of saga transactions started") .register(meterRegistry); this.sagaCompletedCounter = Counter.builder("saga.completed") .description("Number of saga transactions completed successfully") .register(meterRegistry); this.sagaFailedCounter = Counter.builder("saga.failed") .description("Number of saga transactions failed") .register(meterRegistry); this.sagaDurationTimer = Timer.builder("saga.duration") .description("Saga transaction duration") .register(meterRegistry); } public void recordSagaStarted(String sagaType) { sagaStartedCounter.increment(Tags.of("type", sagaType)); } public void recordSagaCompleted(String sagaType, Duration duration) { sagaCompletedCounter.increment(Tags.of("type", sagaType)); sagaDurationTimer.record(duration); } public void recordSagaFailed(String sagaType, String reason) { sagaFailedCounter.increment(Tags.of("type", sagaType, "reason", reason)); } }
Error handling i retry strategies
@Service public class ResilientSagaOrchestrator { @Retryable(value = {TransientException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2)) public void executeSagaStep(SagaStep step) { try { step.execute(); } catch (PermanentException e) { // Nie retryujemy - od razu kompensacja throw e; } catch (TransientException e) { // Spring Retry automatycznie powtórzy throw e; } } @Recover public void recoverFromFailedStep(TransientException ex, SagaStep step) { log.error("Saga step failed after retries, initiating compensation", ex); initiateSagaCompensation(step.getSagaId()); } // Circuit breaker dla external services @CircuitBreaker(name = "payment-service", fallbackMethod = "paymentServiceFallback") public PaymentResult callPaymentService(PaymentRequest request) { return paymentServiceClient.processPayment(request); } public PaymentResult paymentServiceFallback(PaymentRequest request, Exception ex) { // Fallback strategy - może queue for later processing log.warn("Payment service unavailable, queuing for later", ex); paymentQueue.enqueue(request); return PaymentResult.pending(request.getId()); } }
Performance considerations
Kluczowe optymalizacje:
@Configuration public class SagaPerformanceConfig { @Bean @ConfigurationProperties("saga.async") public ThreadPoolTaskExecutor sagaExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(1000); executor.setThreadNamePrefix("saga-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); return executor; } // Async saga step execution @Async("sagaExecutor") public CompletableFutureexecuteStepAsync(SagaStep step) { return CompletableFuture.runAsync(() -> { try { step.execute(); } catch (Exception e) { log.error("Async saga step failed", e); throw new CompletionException(e); } }); } }
Choreography dla loose coupling i prostych workflows (3-4 kroki). Orchestration dla complex business logic i gdy potrzebujesz centralnego kontroli nad procesem.
Używaj test containers dla integration testów, mock external services, testuj compensation logic osobno. Contract testing z Pact dla inter-service communication.
Implementuj retry z exponential backoff, loguj failures do monitoring system, w ostateczności manual intervention. Niektóre compensations mogą wymagać eventual consistency.
Idempotent operations + deduplication keys. Sprawdzaj stan przed wykonaniem operacji. Używaj message ordering w Kafka z partition keys.
Saga zapewnia eventual consistency zamiast immediate consistency. System może być temporarily inconsistent między krokami – projektuj UI/UX z tym na uwadze.
Event store z complete audit trail, monitoring dashboards z saga state, timeout mechanisms z automatic compensation triggers, health checks dla każdego kroku.
Tak, ale wymaga careful design. Partition Kafka topics, horizontal scaling orchestratorów, proper error handling. Netflix używa saga w massive scale.
Przydatne zasoby
- Microservices.io – Saga Pattern
- Spring Boot 2.0 Documentation
- Apache Kafka Documentation
- Martin Fowler – Patterns of Distributed Systems
🚀 Zadanie dla Ciebie
Zaprojektuj i zaimplementuj saga pattern dla procesu onboarding nowego użytkownika: 1) Create account, 2) Send welcome email, 3) Setup default preferences, 4) Notify analytics service. Uwzględnij compensation actions dla każdego kroku i monitoring metrics. Przetestuj scenariusz partial failure.
Saga pattern revolutionizes jak approached distributed transactions w 2018 roku. W miarę jak mikrousługi stają się standardem, understanding tego wzorca becomes essential dla każdego software architect working z distributed systems.