Real World Example

Payment Gateways

Every Indian checkout (Flipkart, Swiggy, BookMyShow) calls a payment gateway such as Razorpay, PayU or Cashfree from the backend. The flow: create a payment order on the gateway, the user pays in the app, then the backend verifies the result. ShopEase talks to a Razorpay-style API (simplified):

React app            ShopEase backend                     Payment gateway
   | POST /api/orders/1001/pay |                                  |
   |-------------------------->| POST /v1/orders (amount, receipt)|
   |                           |--------------------------------->|
   |                           |<---- 200 {id:"order_Q7x", ...} --|
   |<--- {gatewayOrderId} -----|                                  |
   |  user pays in checkout UI ------------------------------->   |
   | POST /api/payments/verify |                                  |
   |-------------------------->| verify signature, mark PAID      |

ShopEase: Payment Client

public record GatewayOrderRequest(long amount, String currency, String receipt) {}   // amount in paise
public record GatewayOrder(String id, long amount, String currency, String status) {}

@Service
public class PaymentService {
    private static final Logger log = LoggerFactory.getLogger(PaymentService.class);
    private final RestClient client;

    public PaymentService(RestClient paymentClient) { this.client = paymentClient; }

    public GatewayOrder createGatewayOrder(String orderNumber, BigDecimal amountInr) {
        long paise = amountInr.movePointRight(2).longValueExact();
        GatewayOrder order = client.post()
                .uri("/v1/orders")
                .header("Idempotency-Key", orderNumber)        // safe to retry
                .body(new GatewayOrderRequest(paise, "INR", orderNumber))
                .retrieve()
                .onStatus(HttpStatusCode::isError, (req, res) -> {
                    throw new PaymentFailedException("Gateway answered " + res.getStatusCode());
                })
                .body(GatewayOrder.class);
        log.info("Created gateway order {} for {}", order.id(), orderNumber);
        return order;
    }
}

@RestController
@RequestMapping("/api/orders")
class OrderPaymentController {
    private final PaymentService payments;
    private final OrderService orders;
    OrderPaymentController(PaymentService payments, OrderService orders) { this.payments = payments; this.orders = orders; }

    @PostMapping("/{orderNumber}/pay")
    Map<String, Object> pay(@PathVariable String orderNumber) {
        BigDecimal total = orders.totalOf(orderNumber);
        GatewayOrder g = payments.createGatewayOrder(orderNumber, total);
        return Map.of("orderNumber", orderNumber, "gatewayOrderId", g.id(), "amount", total);
    }
}

Example

curl -X POST localhost:8080/api/orders/SE-1001/pay

Example output

{"orderNumber":"SE-1001","gatewayOrderId":"order_Q7xK2mPz81","amount":1249.00}

Example

When the gateway is down, the client gets a clean error from the global handler, not a stack trace:

curl -X POST localhost:8080/api/orders/SE-1002/pay

Example output

{"type":"https://shopease.in/errors/payment-failed","title":"Payment failed","status":402,"detail":"Gateway answered 503 SERVICE_UNAVAILABLE","instance":"/api/orders/SE-1002/pay","errorCode":"PAYMENT_FAILED"}

Note: Always send money as an integer in the smallest unit (paise) or as BigDecimal; never use double for amounts.