Careeroza
Real World Example
Food Delivery Checkout (Swiggy / Zomato Style)
A food order touches restaurant, payment, delivery-partner and notification services. Payment providers such as Razorpay and Stripe accept an idempotency key, and the order flow is a saga:
App --POST /orders (Idempotency-Key)--> order-service
| local tx: orders + outbox(OrderPlaced)
v
Kafka order-events --> restaurant-service : accept or reject (kitchen closed?)
--> payment-service : capture UPI payment (idempotent on orderId)
--> delivery-service : assign rider (timeout 3 min)
Rider not found in 3 min -> orchestrator: RefundPayment, NotifyRestaurant(cancel),
order CANCELLED, push "Sorry, no delivery partner nearby"Payment Consumer with All Three Ideas
@Component
@RequiredArgsConstructor
@Slf4j
public class PaymentCommandHandler {
private final ProcessedEventRepository processed;
private final PaymentRepository payments;
private final RazorpayClient razorpay;
private final OutboxRepository outbox;
@KafkaListener(topics = "payment-commands", groupId = "payment-service")
@Transactional
public void handle(ChargePayment cmd) {
if (processed.existsById(cmd.commandId())) { // 1. idempotent consumer
log.info("Command {} already handled", cmd.commandId());
return;
}
PaymentResult result = razorpay.capture(cmd.paymentToken(), cmd.amount(),
"order-" + cmd.orderId()); // 2. idempotency key at the provider
payments.save(Payment.of(cmd.orderId(), result));
outbox.save(OutboxEvent.of(cmd.orderId(), // 3. reply via the outbox
result.captured() ? new PaymentCompleted(cmd.orderId(), result.id())
: new PaymentDeclined(cmd.orderId(), result.reason())));
processed.save(new ProcessedEvent(cmd.commandId()));
}
}| Problem | Pattern that solves it |
|---|---|
| No transaction across order, payment, delivery | Saga with compensations |
| Order saved but event lost | Transactional outbox |
| Kafka delivers the command twice | Idempotent consumer (processed_events) |
| User double-taps "Pay" | Idempotency-Key header + unique constraint |