Saga Pattern
Definition

Choreography vs Orchestration

Choreography Implementation
// CHOREOGRAPHY: Event-driven saga
// Step 1: Order Service
@Service
class OrderService {
@Transactional
public void createOrder(CreateOrderCommand cmd) {
Order order = new Order(cmd.getCustomerId(), cmd.getItems());
order.setStatus(OrderStatus.PENDING);
orderRepository.save(order);
// Publish event to trigger next step
eventPublisher.publish(new OrderCreatedEvent(order));
}
@EventHandler
void on(PaymentFailedEvent event) {
// Compensate: Cancel order
Order order = orderRepository.findById(event.getOrderId()).orElseThrow();
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
}
// Step 2: Inventory Service
@Service
class InventoryService {
@EventHandler
void on(OrderCreatedEvent event) {
try {
reserveInventory(event.getItems());
eventPublisher.publish(new InventoryReservedEvent(event.getOrderId()));
} catch (InsufficientInventoryException e) {
eventPublisher.publish(new InventoryReservationFailedEvent(event.getOrderId()));
}
}
@EventHandler
void on(PaymentFailedEvent event) {
// Compensate: Release reserved inventory
releaseInventory(event.getOrderId());
}
}
// Step 3: Payment Service
@Service
class PaymentService {
@EventHandler
void on(InventoryReservedEvent event) {
try {
chargeCustomer(event.getOrderId());
eventPublisher.publish(new PaymentCompletedEvent(event.getOrderId()));
} catch (PaymentFailedException e) {
eventPublisher.publish(new PaymentFailedEvent(event.getOrderId()));
}
}
}
Orchestration Implementation
// ORCHESTRATION: Central saga coordinator
@Component
class OrderSagaOrchestrator {
@SagaStart
public void startSaga(CreateOrderCommand cmd) {
String sagaId = UUID.randomUUID().toString();
// Step 1: Create order
SagaStep createOrder = new SagaStep(
() -> orderService.createOrder(cmd),
() -> orderService.cancelOrder(cmd.getOrderId())
);
// Step 2: Reserve inventory
SagaStep reserveInventory = new SagaStep(
() -> inventoryService.reserve(cmd.getItems()),
() -> inventoryService.release(cmd.getItems())
);
// Step 3: Process payment
SagaStep processPayment = new SagaStep(
() -> paymentService.charge(cmd.getCustomerId(), cmd.getTotal()),
() -> paymentService.refund(cmd.getCustomerId(), cmd.getTotal())
);
// Execute saga
saga.execute(sagaId, List.of(createOrder, reserveInventory, processPayment));
}
}
// Generic Saga Executor
class SagaExecutor {
public void execute(String sagaId, List<SagaStep> steps) {
List<SagaStep> completedSteps = new ArrayList<>();
try {
for (SagaStep step : steps) {
step.execute();
completedSteps.add(step);
saveSagaState(sagaId, step, SagaStepStatus.COMPLETED);
}
} catch (Exception e) {
// Compensate in reverse order
Collections.reverse(completedSteps);
for (SagaStep step : completedSteps) {
try {
step.compensate();
saveSagaState(sagaId, step, SagaStepStatus.COMPENSATED);
} catch (Exception compensationError) {
// Log and alert - may need manual intervention
saveSagaState(sagaId, step, SagaStepStatus.COMPENSATION_FAILED);
}
}
}
}
}
Saga State Management
// SAGA STATE: Track progress for recovery
@Entity
class SagaState {
@Id
private String sagaId;
@Enumerated(EnumType.STRING)
private SagaStatus status; // STARTED, COMPLETED, COMPENSATING, FAILED
@OneToMany
private List<SagaStepState> steps;
private Instant startedAt;
private Instant completedAt;
}
@Entity
class SagaStepState {
@Id
private String stepId;
private String sagaId;
private String stepName;
@Enumerated(EnumType.STRING)
private SagaStepStatus status; // PENDING, COMPLETED, FAILED, COMPENSATED
private String payload; // For replay
private Instant timestamp;
}
// Recovery: Resume saga after crash
@Service
class SagaRecoveryService {
@Scheduled(fixedDelay = 60000) // Every minute
public void recoverStuckSagas() {
List<SagaState> stuckSagas = sagaRepository
.findByStatusAndStartedAtBefore(
SagaStatus.STARTED,
Instant.now().minus(5, ChronoUnit.MINUTES)
);
for (SagaState saga : stuckSagas) {
// Determine last completed step
SagaStepState lastCompleted = saga.getSteps().stream()
.filter(s -> s.getStatus() == SagaStepStatus.COMPLETED)
.max(Comparator.comparing(SagaStepState::getTimestamp))
.orElse(null);
// Resume from last completed step
resumeSaga(saga, lastCompleted);
}
}
}
Error Handling

Tips & Tricks
