Failure Scenarios

What Can Go Wrong and How ShopEase Handles It

FailureWhat the user seesHow it is handled
App retries POST /api/orders after a timeoutOne order onlyIdempotency key with a unique constraint; the stored response is replayed
product-service is slow or down"Unable to price your cart, try again" (fast)Timeout 800 ms + circuit breaker; nothing is saved, so nothing to undo
Not enough stockOrder cancelled: "Item went out of stock"StockRejected → saga cancels the order; no payment was taken
Payment declinedOrder cancelled, stock back on salePaymentDeclined → compensation ReleaseStock → CANCELLED
Kafka down for 5 minutesOrder stays PENDING a bit longerEvents wait in the outbox tables; the relay sends them when Kafka is back
order-service Pod crashes mid-sagaNothing visibleSaga state is in the DB; another Pod continues on the next reply; idempotent handlers absorb redelivery
payment-service never repliesOrder cancelled after 15 min, refund if chargedSaga timeout job compensates stuck sagas; Razorpay reconciliation job checks real status
Duplicate Kafka messageNothing visibleprocessed_events table in each consumer
Poison message (cannot be parsed)Nothing visible3 retries with backoff, then dead-letter topic + alert
Bad release of payment-serviceA few users see errors for a minuteCanary analysis aborts the rollout automatically
A whole availability zone goes downSlightly slower responsesPods spread across zones, PodDisruptionBudgets, replicated databases

Saga Timeouts

@Component
@RequiredArgsConstructor
@Slf4j
public class StuckSagaSweeper {
    private final OrderSagaRepository sagas;
    private final OrderSagaOrchestrator orchestrator;

    @Scheduled(fixedDelay = 60_000)
    @SchedulerLock(name = "stuck-saga-sweeper")          // ShedLock: one Pod runs it at a time
    @Transactional
    public void compensateStuckSagas() {
        Instant cutoff = Instant.now().minus(Duration.ofMinutes(15));
        for (OrderSaga saga : sagas.findTop100ByStatusAndUpdatedAtBefore(SagaStatus.RUNNING, cutoff)) {
            log.warn("Saga for order {} stuck at {} since {}; compensating", saga.getOrderId(), saga.getStep(), saga.getUpdatedAt());
            orchestrator.compensate(saga, "TIMEOUT");     // refund (idempotent) + release stock + cancel
        }
    }
}

Tip: A timeout does not mean the payment failed. It may have succeeded slowly. That is why the compensation is an idempotent refund-if-charged, and a reconciliation job compares ShopEase payments with the Razorpay settlement report every night.

Retries and Dead-Letter Topics

@Configuration
public class KafkaErrorHandling {
    @Bean
    DefaultErrorHandler kafkaErrorHandler(KafkaTemplate<Object, Object> template) {
        var recoverer = new DeadLetterPublishingRecoverer(template);         // sends to <topic>.DLT
        var handler = new DefaultErrorHandler(recoverer, new FixedBackOff(1_000L, 3));   // 3 retries, 1 s apart
        handler.addNotRetryableExceptions(DeserializationException.class, ValidationException.class);
        return handler;                                                       // picked up by Spring Boot
    }
}

Example

kubectl -n shopease logs deploy/inventory-service | grep -i "dlt"

Example output

WARN 1 --- [inventory-service] [ntainer#0-0-C-1] [7a1c...-3e9f...] o.s.k.l.DeadLetterPublishingRecoverer : Record order-events-4@18233 sent to order-events.DLT after 4 attempts: ValidationException: quantity must be positive

Exercise

Exercise: Customer support reports: "Order 1077 was charged but shows CANCELLED." Using the tools from this level, list the steps to find out what happened.

Show answer
1. Find the trace: search the logs (Loki/Kibana) for orderId=1077 and copy its traceId.
2. Open the trace in Zipkin/Tempo: see which saga steps ran and how long each took.
3. Check saga_instance for order 1077: step, status, updatedAt.
   e.g. step=CHARGING, status=CANCELLED, reason=TIMEOUT -> payment replied after the 15 min timeout.
4. Check payment-service: payments + refunds for order 1077, and the Razorpay dashboard.
5. If the refund failed or was never sent: look in payment-commands.DLT and the error logs.
6. Fix the data (trigger the idempotent refund), then fix the cause:
   e.g. raise the timeout, make the late PaymentCompleted reply trigger an automatic refund,
   add an alert on "charged but cancelled" orders.