In microservices architecture, maintaining data consistency across services without traditional ACID transactions is a significant challenge. The Saga pattern provides a solution for managing distributed transactions, and Saga Orchestration is one of the two main implementation approaches.
Understanding Saga Orchestration
What is a Saga?
A Saga is a sequence of local transactions where each transaction updates data within a single service. If a transaction fails, the Saga executes compensating transactions to undo the changes made by previous transactions.
Saga Orchestration vs Choreography:
- Orchestration: A central coordinator (orchestrator) manages the entire Saga, telling participants what to do and when
- Choreography: Services communicate through events without a central coordinator
Why Saga Orchestration?
- Centralized control and visibility
- Easier to understand and debug
- Better for complex business workflows
- Reduced coupling between services
Core Components
- Saga Orchestrator: The brain that coordinates the entire process
- Saga Participants: Individual services that execute local transactions
- Compensating Actions: Operations that reverse the effects of completed transactions
- Saga Log: Persistent storage for Saga state and events
Implementation Example: E-commerce Order Processing
Let's build a complete Saga Orchestration system for an order processing workflow.
1. Domain Models and DTOs
// Saga Context - carries data throughout the saga
public class OrderSagaData {
private String sagaId;
private String orderId;
private String customerId;
private BigDecimal amount;
private List<OrderItem> items;
private SagaStatus status;
// constructors, getters, setters
}
// Saga Status
public enum SagaStatus {
STARTED,
ORDER_CREATED,
PAYMENT_PROCESSED,
INVENTORY_RESERVED,
COMPLETED,
COMPENSATING,
FAILED
}
// Commands for saga steps
public interface SagaCommand {
void execute(OrderSagaData data);
void compensate(OrderSagaData data);
}
// Saga Events
public abstract class SagaEvent {
private String sagaId;
private String eventId;
private LocalDateTime timestamp;
// constructors, getters, setters
}
public class PaymentProcessedEvent extends SagaEvent {
private String paymentId;
private boolean success;
private String failureReason;
}
2. Saga Steps Implementation
// Order Creation Step
@Component
public class CreateOrderStep implements SagaCommand {
private final OrderService orderService;
@Override
public void execute(OrderSagaData data) {
try {
Order order = orderService.createOrder(data);
data.setOrderId(order.getId());
data.setStatus(SagaStatus.ORDER_CREATED);
System.out.println("Order created: " + order.getId());
} catch (Exception e) {
throw new SagaExecutionException("Failed to create order", e);
}
}
@Override
public void compensate(OrderSagaData data) {
orderService.cancelOrder(data.getOrderId());
System.out.println("Order cancelled: " + data.getOrderId());
}
}
// Payment Processing Step
@Component
public class ProcessPaymentStep implements SagaCommand {
private final PaymentService paymentService;
@Override
public void execute(OrderSagaData data) {
try {
PaymentResponse response = paymentService.processPayment(
data.getCustomerId(), data.getAmount());
if (!response.isSuccess()) {
throw new SagaExecutionException("Payment failed: " + response.getReason());
}
data.setStatus(SagaStatus.PAYMENT_PROCESSED);
System.out.println("Payment processed for order: " + data.getOrderId());
} catch (Exception e) {
throw new SagaExecutionException("Payment processing failed", e);
}
}
@Override
public void compensate(OrderSagaData data) {
paymentService.refundPayment(data.getOrderId());
System.out.println("Payment refunded for order: " + data.getOrderId());
}
}
// Inventory Reservation Step
@Component
public class ReserveInventoryStep implements SagaCommand {
private final InventoryService inventoryService;
@Override
public void execute(OrderSagaData data) {
try {
boolean reserved = inventoryService.reserveItems(
data.getOrderId(), data.getItems());
if (!reserved) {
throw new SagaExecutionException("Inventory reservation failed");
}
data.setStatus(SagaStatus.INVENTORY_RESERVED);
System.out.println("Inventory reserved for order: " + data.getOrderId());
} catch (Exception e) {
throw new SagaExecutionException("Inventory reservation failed", e);
}
}
@Override
public void compensate(OrderSagaData data) {
inventoryService.releaseItems(data.getOrderId());
System.out.println("Inventory released for order: " + data.getOrderId());
}
}
3. Saga Orchestrator
@Component
public class OrderSagaOrchestrator {
private final List<SagaCommand> sagaSteps;
private final SagaRepository sagaRepository;
public OrderSagaOrchestrator(CreateOrderStep createOrderStep,
ProcessPaymentStep processPaymentStep,
ReserveInventoryStep reserveInventoryStep,
SagaRepository sagaRepository) {
this.sagaSteps = List.of(createOrderStep, processPaymentStep, reserveInventoryStep);
this.sagaRepository = sagaRepository;
}
public void executeSaga(OrderSagaData sagaData) {
sagaData.setSagaId(UUID.randomUUID().toString());
sagaData.setStatus(SagaStatus.STARTED);
sagaRepository.save(sagaData);
int currentStep = 0;
try {
for (SagaCommand step : sagaSteps) {
step.execute(sagaData);
sagaRepository.save(sagaData);
currentStep++;
}
sagaData.setStatus(SagaStatus.COMPLETED);
sagaRepository.save(sagaData);
System.out.println("Saga completed successfully: " + sagaData.getSagaId());
} catch (Exception e) {
System.err.println("Saga failed at step " + currentStep + ": " + e.getMessage());
compensate(sagaData, currentStep - 1);
}
}
private void compensate(OrderSagaData sagaData, int failedStepIndex) {
sagaData.setStatus(SagaStatus.COMPENSATING);
sagaRepository.save(sagaData);
// Execute compensating actions in reverse order
for (int i = failedStepIndex; i >= 0; i--) {
try {
sagaSteps.get(i).compensate(sagaData);
} catch (Exception compException) {
System.err.println("Compensation failed at step " + i + ": " + compException.getMessage());
// Log compensation failure but continue with other compensations
}
}
sagaData.setStatus(SagaStatus.FAILED);
sagaRepository.save(sagaData);
System.out.println("Saga compensation completed: " + sagaData.getSagaId());
}
}
4. Repository and Service Interfaces
// Saga Repository
public interface SagaRepository {
void save(OrderSagaData sagaData);
Optional<OrderSagaData> findById(String sagaId);
List<OrderSagaData> findByStatus(SagaStatus status);
}
// Service Interfaces
public interface OrderService {
Order createOrder(OrderSagaData data);
void cancelOrder(String orderId);
}
public interface PaymentService {
PaymentResponse processPayment(String customerId, BigDecimal amount);
void refundPayment(String orderId);
}
public interface InventoryService {
boolean reserveItems(String orderId, List<OrderItem> items);
void releaseItems(String orderId);
}
5. Spring Boot Configuration
@Configuration
@EnableAsync
public class SagaConfiguration {
@Bean
public OrderSagaOrchestrator orderSagaOrchestrator(CreateOrderStep createOrderStep,
ProcessPaymentStep processPaymentStep,
ReserveInventoryStep reserveInventoryStep,
SagaRepository sagaRepository) {
return new OrderSagaOrchestrator(createOrderStep, processPaymentStep,
reserveInventoryStep, sagaRepository);
}
@Bean
public TaskExecutor sagaTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.setThreadNamePrefix("saga-");
return executor;
}
}
6. REST Controller
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderSagaOrchestrator sagaOrchestrator;
private final TaskExecutor taskExecutor;
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
OrderSagaData sagaData = convertToSagaData(request);
// Execute saga asynchronously
taskExecutor.execute(() -> sagaOrchestrator.executeSaga(sagaData));
return ResponseEntity.accepted()
.body(new OrderResponse(sagaData.getSagaId(), "Order processing started"));
}
@GetMapping("/{sagaId}/status")
public ResponseEntity<SagaStatus> getSagaStatus(@PathVariable String sagaId) {
// Implementation to query saga status from repository
return ResponseEntity.ok(SagaStatus.STARTED); // Simplified
}
private OrderSagaData convertToSagaData(CreateOrderRequest request) {
OrderSagaData data = new OrderSagaData();
data.setCustomerId(request.getCustomerId());
data.setAmount(request.getAmount());
data.setItems(request.getItems());
return data;
}
}
7. Exception Handling
public class SagaExecutionException extends RuntimeException {
public SagaExecutionException(String message) {
super(message);
}
public SagaExecutionException(String message, Throwable cause) {
super(message, cause);
}
}
@ControllerAdvice
public class SagaExceptionHandler {
@ExceptionHandler(SagaExecutionException.class)
public ResponseEntity<ErrorResponse> handleSagaException(SagaExecutionException ex) {
ErrorResponse error = new ErrorResponse("SAGA_ERROR", ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
Best Practices and Considerations
- Idempotency: Ensure all saga steps and compensations are idempotent
- Persistence: Always persist saga state before and after each step
- Timeouts: Implement timeouts for long-running operations
- Monitoring: Track saga progress and implement alerting for failures
- Retry Logic: Implement smart retry mechanisms for transient failures
- Testing: Thoroughly test both happy path and compensation scenarios
// Example of idempotent compensation
@Component
public class IdempotentPaymentStep implements SagaCommand {
private final PaymentService paymentService;
@Override
public void compensate(OrderSagaData data) {
// Check if refund was already processed
if (!paymentService.isRefundProcessed(data.getOrderId())) {
paymentService.refundPayment(data.getOrderId());
}
}
}
Conclusion
Saga Orchestration provides a robust pattern for managing distributed transactions in microservices. The Java implementation shown here demonstrates:
- Clear separation of concerns between orchestration and business logic
- Reliable compensation mechanisms for rollbacks
- Scalable architecture that can handle complex workflows
- Maintainable code through well-defined interfaces and components
This approach ensures data consistency across services while maintaining the autonomy and scalability benefits of microservices architecture. The pattern can be extended with more sophisticated features like saga timeouts, parallel execution of independent steps, and integration with event sourcing for complete auditability.