# Spring Boot SAGA Pattern - Examples ## Table of Contents 1. [E-Commerce Order Processing](#e-commerce-order-processing) 2. [Food Delivery Application](#food-delivery-application) 3. [Travel Booking System](#travel-booking-system) 4. [Banking Transfer System](#banking-transfer-system) 5. [Microservices Choreography Example](#microservices-choreography-example) 6. [Microservices Orchestration Example](#microservices-orchestration-example) --- ## E-Commerce Order Processing Complete example of an order processing system using orchestration-based saga with Axon Framework. ### Architecture Overview ``` Order Service → Payment Service → Inventory Service → Shipment Service → Notification Service ↓ ↓ ↓ ↓ ↓ Compensation Compensation Compensation Compensation Compensation ``` ### Project Structure ``` e-commerce-saga/ ├── order-service/ │ ├── domain/ │ │ ├── model/ │ │ │ └── Order.java │ │ ├── event/ │ │ │ ├── OrderCreatedEvent.java │ │ │ └── OrderCancelledEvent.java │ │ └── command/ │ │ ├── CreateOrderCommand.java │ │ └── CancelOrderCommand.java │ └── saga/ │ └── OrderSaga.java ├── payment-service/ │ ├── domain/ │ │ ├── model/ │ │ │ └── Payment.java │ │ ├── event/ │ │ │ ├── PaymentProcessedEvent.java │ │ │ └── PaymentFailedEvent.java │ │ └── command/ │ │ ├── ProcessPaymentCommand.java │ │ └── RefundPaymentCommand.java │ └── aggregate/ │ └── PaymentAggregate.java ├── inventory-service/ ├── shipment-service/ └── notification-service/ ``` ### Domain Models #### Order Entity ```java @Entity @Table(name = "orders") public class Order { @Id private String orderId; @Column(nullable = false) private String customerId; @Column(nullable = false) private BigDecimal totalAmount; @Enumerated(EnumType.STRING) @Column(nullable = false) private OrderStatus status; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) private List items = new ArrayList<>(); @Column(nullable = false) private Instant createdAt; private Instant completedAt; @Version private Long version; // Constructor public Order() { } public Order(String orderId, String customerId, BigDecimal totalAmount) { this.orderId = orderId; this.customerId = customerId; this.totalAmount = totalAmount; this.status = OrderStatus.PENDING; this.createdAt = Instant.now(); } // Business methods public void markAsProcessing() { if (this.status != OrderStatus.PENDING) { throw new IllegalStateException("Order must be pending to mark as processing"); } this.status = OrderStatus.PROCESSING; } public void markAsCompleted() { if (this.status != OrderStatus.PROCESSING) { throw new IllegalStateException("Order must be processing to complete"); } this.status = OrderStatus.COMPLETED; this.completedAt = Instant.now(); } public void cancel() { if (this.status == OrderStatus.COMPLETED) { throw new IllegalStateException("Cannot cancel completed order"); } this.status = OrderStatus.CANCELLED; } // Getters public String getOrderId() { return orderId; } public String getCustomerId() { return customerId; } public BigDecimal getTotalAmount() { return totalAmount; } public OrderStatus getStatus() { return status; } public List getItems() { return items; } public Instant getCreatedAt() { return createdAt; } public Instant getCompletedAt() { return completedAt; } } public enum OrderStatus { PENDING, PROCESSING, COMPLETED, CANCELLED, FAILED } ``` #### Order Item ```java @Entity @Table(name = "order_items") public class OrderItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String productId; @Column(nullable = false) private String productName; @Column(nullable = false) private Integer quantity; @Column(nullable = false) private BigDecimal unitPrice; @Column(nullable = false) private BigDecimal totalPrice; // Constructors public OrderItem() { } public OrderItem(String productId, String productName, Integer quantity, BigDecimal unitPrice) { this.productId = productId; this.productName = productName; this.quantity = quantity; this.unitPrice = unitPrice; this.totalPrice = unitPrice.multiply(BigDecimal.valueOf(quantity)); } // Getters public Long getId() { return id; } public String getProductId() { return productId; } public String getProductName() { return productName; } public Integer getQuantity() { return quantity; } public BigDecimal getUnitPrice() { return unitPrice; } public BigDecimal getTotalPrice() { return totalPrice; } } ``` ### Commands and Events #### Order Commands ```java public record CreateOrderCommand( @TargetAggregateIdentifier String orderId, String customerId, List items, BigDecimal totalAmount ) {} public record CancelOrderCommand( @TargetAggregateIdentifier String orderId, String reason ) {} public record CompleteOrderCommand( @TargetAggregateIdentifier String orderId ) {} ``` #### Order Events ```java public record OrderCreatedEvent( String orderId, String customerId, List items, BigDecimal totalAmount, Instant timestamp ) {} public record OrderCancelledEvent( String orderId, String reason, Instant timestamp ) {} public record OrderCompletedEvent( String orderId, Instant timestamp ) {} ``` #### Payment Commands ```java public record ProcessPaymentCommand( @TargetAggregateIdentifier String paymentId, String orderId, String customerId, BigDecimal amount, PaymentMethod paymentMethod ) {} public record RefundPaymentCommand( @TargetAggregateIdentifier String paymentId, String orderId, BigDecimal amount, String reason ) {} ``` #### Payment Events ```java public record PaymentProcessedEvent( String paymentId, String orderId, String customerId, BigDecimal amount, Instant timestamp ) {} public record PaymentFailedEvent( String paymentId, String orderId, String reason, Instant timestamp ) {} public record PaymentRefundedEvent( String paymentId, String orderId, BigDecimal amount, Instant timestamp ) {} ``` ### Order Saga Implementation ```java @Saga public class OrderSaga { private static final Logger logger = LoggerFactory.getLogger(OrderSaga.class); @Autowired private transient CommandGateway commandGateway; private String orderId; private String paymentId; private String shipmentId; private boolean compensating = false; @StartSaga @SagaEventHandler(associationProperty = "orderId") public void handle(OrderCreatedEvent event) { this.orderId = event.orderId(); logger.info("Order saga started for orderId: {}", orderId); // Generate payment ID this.paymentId = UUID.randomUUID().toString(); // Send process payment command ProcessPaymentCommand command = new ProcessPaymentCommand( paymentId, event.orderId(), event.customerId(), event.totalAmount(), new PaymentMethod("CREDIT_CARD", Map.of()) ); commandGateway.send(command, (commandMessage, commandResultMessage) -> { if (commandResultMessage.isExceptional()) { logger.error("Payment command failed for orderId: {}", orderId); // Handle command failure } }); } @SagaEventHandler(associationProperty = "orderId") public void handle(PaymentProcessedEvent event) { logger.info("Payment processed for orderId: {}", event.orderId()); if (compensating) { logger.info("Saga is compensating, skipping inventory reservation"); return; } // Send reserve inventory command ReserveInventoryCommand command = new ReserveInventoryCommand( event.orderId(), event.orderId() // Assuming items are tracked by orderId ); commandGateway.send(command); } @SagaEventHandler(associationProperty = "orderId") public void handle(InventoryReservedEvent event) { logger.info("Inventory reserved for orderId: {}", event.orderId()); if (compensating) { logger.info("Saga is compensating, skipping shipment preparation"); return; } // Generate shipment ID this.shipmentId = UUID.randomUUID().toString(); // Send prepare shipment command PrepareShipmentCommand command = new PrepareShipmentCommand( shipmentId, event.orderId(), event.orderId() // Assuming shipment details tracked by orderId ); commandGateway.send(command); } @SagaEventHandler(associationProperty = "orderId") public void handle(ShipmentPreparedEvent event) { logger.info("Shipment prepared for orderId: {}", event.orderId()); if (compensating) { logger.info("Saga is compensating, skipping notification"); return; } // Send notification command SendNotificationCommand command = new SendNotificationCommand( event.orderId(), "ORDER_CONFIRMED", "Your order has been confirmed and is being prepared for shipment." ); commandGateway.send(command); } @SagaEventHandler(associationProperty = "orderId") public void handle(NotificationSentEvent event) { logger.info("Notification sent for orderId: {}", event.orderId()); // Complete the order CompleteOrderCommand command = new CompleteOrderCommand(event.orderId()); commandGateway.send(command); } @EndSaga @SagaEventHandler(associationProperty = "orderId") public void handle(OrderCompletedEvent event) { logger.info("Order saga completed for orderId: {}", event.orderId()); } // Compensation handlers @SagaEventHandler(associationProperty = "orderId") public void handle(PaymentFailedEvent event) { logger.error("Payment failed for orderId: {}, reason: {}", event.orderId(), event.reason()); compensating = true; // Cancel the order CancelOrderCommand command = new CancelOrderCommand( event.orderId(), "Payment failed: " + event.reason() ); commandGateway.send(command); } @SagaEventHandler(associationProperty = "orderId") public void handle(InventoryReservationFailedEvent event) { logger.error("Inventory reservation failed for orderId: {}, reason: {}", event.orderId(), event.reason()); compensating = true; // Refund payment RefundPaymentCommand refundCommand = new RefundPaymentCommand( paymentId, event.orderId(), event.amount(), "Inventory unavailable" ); commandGateway.send(refundCommand); } @SagaEventHandler(associationProperty = "orderId") public void handle(PaymentRefundedEvent event) { logger.info("Payment refunded for orderId: {}", event.orderId()); // Cancel the order CancelOrderCommand command = new CancelOrderCommand( event.orderId(), "Inventory unavailable - payment refunded" ); commandGateway.send(command); } @EndSaga @SagaEventHandler(associationProperty = "orderId") public void handle(OrderCancelledEvent event) { logger.info("Order saga ended with cancellation for orderId: {}", event.orderId()); } } ``` ### Payment Aggregate ```java @Aggregate public class PaymentAggregate { @AggregateIdentifier private String paymentId; private String orderId; private BigDecimal amount; private PaymentStatus status; public PaymentAggregate() { } @CommandHandler public PaymentAggregate(ProcessPaymentCommand command) { // Validate payment if (command.amount().compareTo(BigDecimal.ZERO) <= 0) { apply(new PaymentFailedEvent( command.paymentId(), command.orderId(), "Invalid payment amount", Instant.now() )); return; } // Simulate payment gateway call boolean paymentSuccessful = processPaymentWithGateway(command); if (paymentSuccessful) { apply(new PaymentProcessedEvent( command.paymentId(), command.orderId(), command.customerId(), command.amount(), Instant.now() )); } else { apply(new PaymentFailedEvent( command.paymentId(), command.orderId(), "Payment gateway declined", Instant.now() )); } } @EventSourcingHandler public void on(PaymentProcessedEvent event) { this.paymentId = event.paymentId(); this.orderId = event.orderId(); this.amount = event.amount(); this.status = PaymentStatus.PROCESSED; } @EventSourcingHandler public void on(PaymentFailedEvent event) { this.paymentId = event.paymentId(); this.orderId = event.orderId(); this.status = PaymentStatus.FAILED; } @CommandHandler public void handle(RefundPaymentCommand command) { if (this.status != PaymentStatus.PROCESSED) { throw new IllegalStateException("Can only refund processed payments"); } apply(new PaymentRefundedEvent( command.paymentId(), command.orderId(), command.amount(), Instant.now() )); } @EventSourcingHandler public void on(PaymentRefundedEvent event) { this.status = PaymentStatus.REFUNDED; } private boolean processPaymentWithGateway(ProcessPaymentCommand command) { // Simulate payment gateway integration // In real implementation, call actual payment gateway API try { // Simulate network delay Thread.sleep(100); // 90% success rate for demonstration return Math.random() > 0.1; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } } enum PaymentStatus { PENDING, PROCESSED, FAILED, REFUNDED } ``` ### Inventory Aggregate ```java @Aggregate public class InventoryAggregate { @AggregateIdentifier private String inventoryId; private String orderId; private Map reservedItems = new HashMap<>(); public InventoryAggregate() { } @CommandHandler public InventoryAggregate(ReserveInventoryCommand command) { // Check inventory availability boolean available = checkInventoryAvailability(command.items()); if (available) { apply(new InventoryReservedEvent( UUID.randomUUID().toString(), command.orderId(), command.items(), Instant.now() )); } else { apply(new InventoryReservationFailedEvent( command.orderId(), "Insufficient inventory", BigDecimal.ZERO, // Would calculate from order Instant.now() )); } } @EventSourcingHandler public void on(InventoryReservedEvent event) { this.inventoryId = event.inventoryId(); this.orderId = event.orderId(); this.reservedItems = event.items(); } @CommandHandler public void handle(ReleaseInventoryCommand command) { apply(new InventoryReleasedEvent( this.inventoryId, command.orderId(), this.reservedItems, Instant.now() )); } @EventSourcingHandler public void on(InventoryReleasedEvent event) { this.reservedItems.clear(); } private boolean checkInventoryAvailability(Map items) { // In real implementation, check actual inventory database // For demonstration, 95% availability return Math.random() > 0.05; } } ``` ### Order Service Implementation ```java @Service public class OrderService { private final CommandGateway commandGateway; private final OrderRepository orderRepository; public OrderService(CommandGateway commandGateway, OrderRepository orderRepository) { this.commandGateway = commandGateway; this.orderRepository = orderRepository; } public String createOrder(CreateOrderRequest request) { String orderId = UUID.randomUUID().toString(); // Create order entity Order order = new Order( orderId, request.customerId(), request.totalAmount() ); // Add order items request.items().forEach(item -> { order.getItems().add(new OrderItem( item.productId(), item.productName(), item.quantity(), item.unitPrice() )); }); // Save order orderRepository.save(order); // Send create order command to start saga CreateOrderCommand command = new CreateOrderCommand( orderId, request.customerId(), request.items(), request.totalAmount() ); commandGateway.send(command); return orderId; } public OrderDTO getOrder(String orderId) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new OrderNotFoundException(orderId)); return OrderDTO.fromEntity(order); } } ``` ### REST Controller ```java @RestController @RequestMapping("/api/orders") public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService = orderService; } @PostMapping public ResponseEntity createOrder( @Valid @RequestBody CreateOrderRequest request) { String orderId = orderService.createOrder(request); return ResponseEntity .status(HttpStatus.CREATED) .body(new OrderResponse(orderId, "Order created successfully")); } @GetMapping("/{orderId}") public ResponseEntity getOrder(@PathVariable String orderId) { OrderDTO order = orderService.getOrder(orderId); return ResponseEntity.ok(order); } } ``` ### Configuration #### Application Properties ```properties # Application spring.application.name=order-service server.port=8080 # Database spring.datasource.url=jdbc:postgresql://localhost:5432/orderdb spring.datasource.username=orderuser spring.datasource.password=orderpass spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect # Axon Configuration axon.axonserver.servers=localhost:8124 axon.serializer.general=jackson axon.serializer.events=jackson axon.serializer.messages=jackson # Actuator management.endpoints.web.exposure.include=health,metrics,info,prometheus management.endpoint.health.show-details=always management.metrics.export.prometheus.enabled=true ``` #### Maven Dependencies ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-validation org.axonframework axon-spring-boot-starter 4.9.0 org.postgresql postgresql runtime org.springframework.boot spring-boot-starter-actuator io.micrometer micrometer-registry-prometheus org.springframework.boot spring-boot-starter-test test org.axonframework axon-test 4.9.0 test ``` --- ## Food Delivery Application Choreography-based saga example using Spring Cloud Stream and Kafka. ### Architecture ``` Order Service ↓ (publishes OrderCreatedEvent) Restaurant Service ↓ (publishes OrderAcceptedEvent / OrderRejectedEvent) Payment Service ↓ (publishes PaymentSuccessEvent / PaymentFailedEvent) Delivery Service ↓ (publishes DeliveryAssignedEvent / DeliveryFailedEvent) Notification Service ``` ### Domain Events ```java // Order Events public record OrderCreatedEvent( String orderId, String customerId, String restaurantId, List items, BigDecimal totalAmount, DeliveryAddress deliveryAddress, Instant timestamp ) {} public record OrderCancelledEvent( String orderId, String reason, Instant timestamp ) {} // Restaurant Events public record OrderAcceptedEvent( String orderId, String restaurantId, int estimatedPreparationTime, Instant timestamp ) {} public record OrderRejectedEvent( String orderId, String restaurantId, String reason, Instant timestamp ) {} // Payment Events public record PaymentSuccessEvent( String orderId, String paymentId, BigDecimal amount, Instant timestamp ) {} public record PaymentFailedEvent( String orderId, String paymentId, String reason, Instant timestamp ) {} // Delivery Events public record DeliveryAssignedEvent( String orderId, String deliveryPersonId, String deliveryPersonName, Instant estimatedDeliveryTime, Instant timestamp ) {} public record DeliveryFailedEvent( String orderId, String reason, Instant timestamp ) {} ``` ### Order Service ```java @Service public class FoodOrderService { private final StreamBridge streamBridge; private final OrderRepository orderRepository; public FoodOrderService(StreamBridge streamBridge, OrderRepository orderRepository) { this.streamBridge = streamBridge; this.orderRepository = orderRepository; } public String createOrder(CreateFoodOrderRequest request) { String orderId = UUID.randomUUID().toString(); // Create and save order FoodOrder order = new FoodOrder( orderId, request.customerId(), request.restaurantId(), request.items(), calculateTotal(request.items()), request.deliveryAddress() ); orderRepository.save(order); // Publish order created event OrderCreatedEvent event = new OrderCreatedEvent( orderId, request.customerId(), request.restaurantId(), request.items(), order.getTotalAmount(), request.deliveryAddress(), Instant.now() ); streamBridge.send("order-events", event); return orderId; } @Bean public Consumer handleOrderRejected() { return event -> { FoodOrder order = orderRepository.findById(event.orderId()) .orElseThrow(); order.cancel("Restaurant rejected: " + event.reason()); orderRepository.save(order); // Publish cancellation event OrderCancelledEvent cancelEvent = new OrderCancelledEvent( event.orderId(), event.reason(), Instant.now() ); streamBridge.send("order-events", cancelEvent); }; } @Bean public Consumer handlePaymentFailed() { return event -> { FoodOrder order = orderRepository.findById(event.orderId()) .orElseThrow(); order.cancel("Payment failed: " + event.reason()); orderRepository.save(order); // Notify restaurant to cancel preparation RestaurantCancelOrderEvent cancelEvent = new RestaurantCancelOrderEvent( event.orderId(), "Payment failed", Instant.now() ); streamBridge.send("restaurant-events", cancelEvent); }; } private BigDecimal calculateTotal(List items) { return items.stream() .map(item -> item.price().multiply(BigDecimal.valueOf(item.quantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); } } ``` ### Restaurant Service ```java @Service public class RestaurantService { private final StreamBridge streamBridge; private final RestaurantRepository restaurantRepository; public RestaurantService(StreamBridge streamBridge, RestaurantRepository restaurantRepository) { this.streamBridge = streamBridge; this.restaurantRepository = restaurantRepository; } @Bean public Consumer handleOrderCreated() { return event -> { Restaurant restaurant = restaurantRepository .findById(event.restaurantId()) .orElseThrow(); // Check if restaurant can accept order if (restaurant.canAcceptOrder(event.items())) { // Accept order int preparationTime = restaurant.estimatePreparationTime(event.items()); OrderAcceptedEvent acceptedEvent = new OrderAcceptedEvent( event.orderId(), event.restaurantId(), preparationTime, Instant.now() ); streamBridge.send("restaurant-events", acceptedEvent); // Start preparing order restaurant.startPreparingOrder(event.orderId(), event.items()); restaurantRepository.save(restaurant); } else { // Reject order OrderRejectedEvent rejectedEvent = new OrderRejectedEvent( event.orderId(), event.restaurantId(), "Items unavailable or restaurant closed", Instant.now() ); streamBridge.send("restaurant-events", rejectedEvent); } }; } @Bean public Consumer handleCancelOrder() { return event -> { Restaurant restaurant = restaurantRepository .findByOrderId(event.orderId()) .orElse(null); if (restaurant != null) { restaurant.cancelOrderPreparation(event.orderId()); restaurantRepository.save(restaurant); } }; } } ``` ### Payment Service ```java @Service public class FoodPaymentService { private final StreamBridge streamBridge; private final PaymentGateway paymentGateway; public FoodPaymentService(StreamBridge streamBridge, PaymentGateway paymentGateway) { this.streamBridge = streamBridge; this.paymentGateway = paymentGateway; } @Bean public Consumer handleOrderAccepted() { return event -> { String paymentId = UUID.randomUUID().toString(); try { // Process payment PaymentResult result = paymentGateway.processPayment( paymentId, event.orderId(), getOrderAmount(event.orderId()) ); if (result.isSuccess()) { PaymentSuccessEvent successEvent = new PaymentSuccessEvent( event.orderId(), paymentId, result.amount(), Instant.now() ); streamBridge.send("payment-events", successEvent); } else { PaymentFailedEvent failedEvent = new PaymentFailedEvent( event.orderId(), paymentId, result.errorMessage(), Instant.now() ); streamBridge.send("payment-events", failedEvent); } } catch (Exception e) { PaymentFailedEvent failedEvent = new PaymentFailedEvent( event.orderId(), paymentId, "Payment processing error: " + e.getMessage(), Instant.now() ); streamBridge.send("payment-events", failedEvent); } }; } private BigDecimal getOrderAmount(String orderId) { // Fetch order amount from order service or database return BigDecimal.valueOf(50.00); // Placeholder } } ``` ### Delivery Service ```java @Service public class DeliveryService { private final StreamBridge streamBridge; private final DeliveryPersonRepository deliveryPersonRepository; public DeliveryService(StreamBridge streamBridge, DeliveryPersonRepository deliveryPersonRepository) { this.streamBridge = streamBridge; this.deliveryPersonRepository = deliveryPersonRepository; } @Bean public Consumer handlePaymentSuccess() { return event -> { // Find available delivery person DeliveryPerson deliveryPerson = deliveryPersonRepository.findAvailableNearby() .orElse(null); if (deliveryPerson != null) { // Assign delivery deliveryPerson.assignOrder(event.orderId()); deliveryPersonRepository.save(deliveryPerson); Instant estimatedDelivery = Instant.now() .plus(30, ChronoUnit.MINUTES); DeliveryAssignedEvent assignedEvent = new DeliveryAssignedEvent( event.orderId(), deliveryPerson.getId(), deliveryPerson.getName(), estimatedDelivery, Instant.now() ); streamBridge.send("delivery-events", assignedEvent); } else { // No delivery person available DeliveryFailedEvent failedEvent = new DeliveryFailedEvent( event.orderId(), "No delivery person available", Instant.now() ); streamBridge.send("delivery-events", failedEvent); } }; } } ``` ### Notification Service ```java @Service public class NotificationService { private final EmailService emailService; private final SmsService smsService; public NotificationService(EmailService emailService, SmsService smsService) { this.emailService = emailService; this.smsService = smsService; } @Bean public Consumer handleOrderAccepted() { return event -> { String message = String.format( "Your order #%s has been accepted by the restaurant. " + "Estimated preparation time: %d minutes", event.orderId(), event.estimatedPreparationTime() ); sendNotification(event.orderId(), message); }; } @Bean public Consumer handlePaymentSuccess() { return event -> { String message = String.format( "Payment of $%.2f for order #%s processed successfully", event.amount(), event.orderId() ); sendNotification(event.orderId(), message); }; } @Bean public Consumer handleDeliveryAssigned() { return event -> { String message = String.format( "Your order #%s has been assigned to delivery person %s. " + "Estimated delivery: %s", event.orderId(), event.deliveryPersonName(), event.estimatedDeliveryTime() ); sendNotification(event.orderId(), message); }; } @Bean public Consumer handleOrderCancelled() { return event -> { String message = String.format( "Your order #%s has been cancelled. Reason: %s", event.orderId(), event.reason() ); sendNotification(event.orderId(), message); }; } private void sendNotification(String orderId, String message) { // Get customer contact info from order // Send email and SMS emailService.sendEmail(orderId, message); smsService.sendSms(orderId, message); } } ``` ### Spring Cloud Stream Configuration ```yaml spring: cloud: stream: bindings: # Order Service order-events: destination: food-order-events contentType: application/json # Restaurant Service restaurant-events: destination: food-restaurant-events contentType: application/json # Payment Service payment-events: destination: food-payment-events contentType: application/json # Delivery Service delivery-events: destination: food-delivery-events contentType: application/json kafka: binder: brokers: localhost:9092 auto-create-topics: true bindings: order-events: producer: configuration: acks: all retries: 3 restaurant-events: consumer: configuration: max-poll-records: 10 ``` --- ## Travel Booking System Complex orchestration example with multiple compensations. ### Architecture ``` Flight Booking → Hotel Booking → Car Rental → Payment → Confirmation ↓ ↓ ↓ ↓ ↓ Cancel Flight Cancel Hotel Cancel Car Refund Cancel All ``` ### Travel Saga ```java @Saga public class TravelBookingSaga { @Autowired private transient CommandGateway commandGateway; private String bookingId; private String flightReservationId; private String hotelReservationId; private String carRentalReservationId; private String paymentId; private boolean compensating = false; @StartSaga @SagaEventHandler(associationProperty = "bookingId") public void handle(TravelBookingStartedEvent event) { this.bookingId = event.bookingId(); // Step 1: Book flight this.flightReservationId = UUID.randomUUID().toString(); BookFlightCommand command = new BookFlightCommand( flightReservationId, event.bookingId(), event.flightDetails() ); commandGateway.send(command); } @SagaEventHandler(associationProperty = "bookingId") public void handle(FlightBookedEvent event) { if (compensating) return; // Step 2: Book hotel this.hotelReservationId = UUID.randomUUID().toString(); BookHotelCommand command = new BookHotelCommand( hotelReservationId, event.bookingId(), event.hotelDetails() ); commandGateway.send(command); } @SagaEventHandler(associationProperty = "bookingId") public void handle(HotelBookedEvent event) { if (compensating) return; // Step 3: Rent car this.carRentalReservationId = UUID.randomUUID().toString(); RentCarCommand command = new RentCarCommand( carRentalReservationId, event.bookingId(), event.carRentalDetails() ); commandGateway.send(command); } @SagaEventHandler(associationProperty = "bookingId") public void handle(CarRentedEvent event) { if (compensating) return; // Step 4: Process payment this.paymentId = UUID.randomUUID().toString(); ProcessTravelPaymentCommand command = new ProcessTravelPaymentCommand( paymentId, event.bookingId(), calculateTotalAmount() ); commandGateway.send(command); } @SagaEventHandler(associationProperty = "bookingId") public void handle(TravelPaymentProcessedEvent event) { if (compensating) return; // Step 5: Confirm booking ConfirmTravelBookingCommand command = new ConfirmTravelBookingCommand( event.bookingId() ); commandGateway.send(command); } @EndSaga @SagaEventHandler(associationProperty = "bookingId") public void handle(TravelBookingConfirmedEvent event) { // Saga completed successfully } // Compensation handlers @SagaEventHandler(associationProperty = "bookingId") public void handle(FlightBookingFailedEvent event) { compensating = true; CancelTravelBookingCommand command = new CancelTravelBookingCommand( event.bookingId(), "Flight booking failed: " + event.reason() ); commandGateway.send(command); } @SagaEventHandler(associationProperty = "bookingId") public void handle(HotelBookingFailedEvent event) { compensating = true; // Cancel flight CancelFlightCommand cancelFlight = new CancelFlightCommand( flightReservationId, event.bookingId() ); commandGateway.send(cancelFlight); } @SagaEventHandler(associationProperty = "bookingId") public void handle(FlightCancelledEvent event) { if (!compensating) return; // After flight cancelled, cancel entire booking CancelTravelBookingCommand command = new CancelTravelBookingCommand( event.bookingId(), "Hotel booking failed - flight cancelled" ); commandGateway.send(command); } @SagaEventHandler(associationProperty = "bookingId") public void handle(CarRentalFailedEvent event) { compensating = true; // Cancel hotel CancelHotelCommand cancelHotel = new CancelHotelCommand( hotelReservationId, event.bookingId() ); commandGateway.send(cancelHotel); } @SagaEventHandler(associationProperty = "bookingId") public void handle(HotelCancelledEvent event) { if (!compensating) return; // Cancel flight CancelFlightCommand cancelFlight = new CancelFlightCommand( flightReservationId, event.bookingId() ); commandGateway.send(cancelFlight); } @SagaEventHandler(associationProperty = "bookingId") public void handle(TravelPaymentFailedEvent event) { compensating = true; // Cancel car rental CancelCarRentalCommand cancelCar = new CancelCarRentalCommand( carRentalReservationId, event.bookingId() ); commandGateway.send(cancelCar); } @SagaEventHandler(associationProperty = "bookingId") public void handle(CarRentalCancelledEvent event) { if (!compensating) return; // Cancel hotel CancelHotelCommand cancelHotel = new CancelHotelCommand( hotelReservationId, event.bookingId() ); commandGateway.send(cancelHotel); } @EndSaga @SagaEventHandler(associationProperty = "bookingId") public void handle(TravelBookingCancelledEvent event) { // Saga ended with cancellation } private BigDecimal calculateTotalAmount() { // Calculate total from all bookings return BigDecimal.valueOf(1500.00); // Placeholder } } ``` This examples file demonstrates practical implementations of the Saga Pattern in various scenarios. Each example shows: 1. Complete domain models 2. Commands and events 3. Saga coordination logic 4. Compensation transactions 5. Service implementations 6. Configuration and dependencies The examples progress from simple to complex, showing both choreography and orchestration approaches with realistic business scenarios.