Skip to content

Saga Pattern


Definition

Saga Pattern Definition


Choreography vs Orchestration

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

Saga Error Handling


Tips & Tricks

Tips and Tricks