DEV Community

Cover image for How Razorpay Ensures Your Payment Succeeds β€” Even If Your Internet Drops
ZeeshanAli-0704
ZeeshanAli-0704

Posted on

How Razorpay Ensures Your Payment Succeeds β€” Even If Your Internet Drops

πŸ“‘ Table of Contents

  1. How Razorpay Ensures Your Payment Succeeds Even If Your Internet Drops After You Click "Pay Now"
  2. 🚩 The Real World Problem
  3. 🧩 Step 1: Identifying the Core Problem
  4. βš™οΈ Step 2: The Reliable Payment Flow β€” End to End
  5. 🧠 Step 3: Key Engineering Concepts
  6. πŸ—οΈ Step 4: System Architecture Diagram
  7. 🧩 Step 5: UML Sequence Diagram
  8. 🧱 Step 6: Suggested Tech Stack
  9. πŸ” Step 7: Handling Failures Gracefully
  10. πŸ”’ Step 8: Security and Compliance
  11. 🧠 Step 9: Final Takeaways
  12. Razorpays Reliability Recipe
  13. πŸ“Š Final Summary

How Razorpay Ensures Your Payment Succeeds β€” Even If Your Internet Drops After You Click "Pay Now"


🚩 The Real World Problem

Imagine this:
You’re buying something online, click β€œPay Now”, and suddenly your internet disconnects.

Now you’re stuck wondering β€”

  • β€œDid my payment go through?”
  • β€œWill I get charged twice if I retry?”
  • β€œHow does the app even know what happened?”

This situation occurs millions of times a day, and yet companies like Razorpay, Stripe, or PayPal handle it gracefully β€” without double-charging users or losing transactions.

Let’s see how they design for reliability, idempotency, and consistency even when your network vanishes mid-payment.


🧩 Step 1: Identifying the Core Problem

When you initiate a payment:

  • Your app sends the payment request.
  • Razorpay talks to your bank.
  • But your client may drop offline before getting the result.

Without protection:

  • The app might show β€œPayment Failed” even though the amount is debited.
  • The user might retry and get charged twice.

Hence, we need a fault-tolerant payment flow that ensures:

Every payment request is processed exactly once, and the final state is always recoverable β€” even if the user disappears.


βš™οΈ Step 2: The Reliable Payment Flow β€” End to End

Let’s walk through what happens behind the scenes.


🧱 Client β†’ Merchant Backend (Create Order)

Every transaction starts by creating a unique Order.
This acts as an idempotency key β€” so retries never create duplicates.

Request:

POST /api/payments/create-order
Content-Type: application/json

{
  "orderId": "ORD_12345",
  "amount": 49900,
  "currency": "INR"
}
Enter fullscreen mode Exit fullscreen mode

Backend Implementation (Node.js):

app.post("/api/payments/create-order", async (req, res) => {
  const { orderId, amount, currency } = req.body;

  const response = await axios.post("https://api.razorpay.com/v1/orders", {
    amount,
    currency,
    receipt: orderId,
    payment_capture: 1
  }, {
    auth: { username: process.env.RAZORPAY_KEY, password: process.env.RAZORPAY_SECRET }
  });

  await db.orders.insert({
    orderId,
    razorpayOrderId: response.data.id,
    status: "CREATED",
    amount
  });

  res.json({ razorpayOrderId: response.data.id, key: process.env.RAZORPAY_KEY });
});
Enter fullscreen mode Exit fullscreen mode

βœ… What happens:

  • Your backend creates an order in Razorpay.
  • The unique razorpayOrderId ensures idempotency.
  • Status = β€œCREATED” is stored in the DB.

πŸ’³ Client β†’ Razorpay Checkout

Your app opens Razorpay’s secure Checkout window.

Frontend Example:

const options = {
  key: RAZORPAY_KEY,
  amount: order.amount,
  currency: "INR",
  order_id: order.razorpayOrderId,
  handler: async function (response) {
    await verifyPayment(response);
  }
};
const rzp = new Razorpay(options);
rzp.open();
Enter fullscreen mode Exit fullscreen mode

⚠️ Important:
This handler() runs only if the browser is alive.
If the internet drops here, Razorpay continues processing the payment in the background β€” but the app won’t know immediately.


🏦 Razorpay β†’ Bank / UPI Network

Razorpay securely forwards your payment request to the bank or UPI system over PCI-DSS compliant channels.

The bank:

  • Processes the payment.
  • Sends the result (success/failure) back to Razorpay.

This happens completely independent of your device’s internet.


πŸ” Razorpay β†’ Merchant Backend (Webhook)

Once Razorpay gets the bank’s result, it triggers a server-to-server webhook to your backend.

Webhook Example:

POST /api/payments/webhook
Content-Type: application/json

{
  "event": "payment.captured",
  "payload": {
    "payment": {
      "id": "pay_29QQoUBi66xm2f",
      "entity": "payment",
      "order_id": "order_DBJOWzybf0sJbb",
      "amount": 49900,
      "status": "captured",
      "method": "upi"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Webhook Handler:

app.post("/api/payments/webhook", async (req, res) => {
  const secret = process.env.RAZORPAY_WEBHOOK_SECRET;
  const signature = req.headers["x-razorpay-signature"];

  const expected = crypto.createHmac("sha256", secret)
    .update(JSON.stringify(req.body))
    .digest("hex");

  if (expected !== signature) return res.status(403).send("Invalid signature");

  const payment = req.body.payload.payment.entity;

  await db.orders.updateOne(
    { razorpayOrderId: payment.order_id },
    { $set: { status: payment.status, paymentId: payment.id } }
  );

  res.status(200).send("OK");
});
Enter fullscreen mode Exit fullscreen mode

βœ… Why this matters:

  • Webhook ensures Razorpay β†’ Merchant communication doesn’t depend on the user’s browser.
  • Even if the user vanishes, your backend receives the final truth.

πŸ”„ Client Reconnects β†’ Merchant Backend

When the user reopens the app:

GET /api/payments/status?orderId=ORD_12345
Enter fullscreen mode Exit fullscreen mode

Backend:

app.get("/api/payments/status", async (req, res) => {
  const order = await db.orders.findOne({ orderId: req.query.orderId });
  res.json({ status: order.status });
});
Enter fullscreen mode Exit fullscreen mode

βœ… Result:
Even after a crash, disconnect, or timeout β€” the app can re-fetch the confirmed payment status directly from the server.


🧠 Step 3: Key Engineering Concepts

Concept Why It’s Needed
Idempotency Ensures retries don’t cause double charges.
Event-driven architecture Webhooks asynchronously notify merchants of results.
Atomic DB Transactions Payment + order update happen together.
Retries with Exponential Backoff Handles transient failures safely.
Queue-based Delivery (Kafka/SQS) Guarantees webhook/event delivery.
Caching (Redis) Enables quick status lookups for reconnecting users.
Audit Logging Every payment event is traceable for reconciliation.

πŸ—οΈ Step 4: System Architecture Diagram

                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                          β”‚         User Client           β”‚
                          β”‚ (Web / Mobile App / Checkout) β”‚
                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                         β”‚
                                         β”‚ (1) Create Order
                                         β–Ό
                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                          β”‚      Merchant Backend         β”‚
                          β”‚ (Spring Boot / Node / Django) β”‚
                          β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                          β”‚ Generates order, stores in DB β”‚
                          β”‚ & calls Razorpay API          β”‚
                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                         β”‚
                                         β”‚ (2) Payment Init
                                         β–Ό
                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                          β”‚          Razorpay API         β”‚
                          β”‚  Connects securely with Bank  β”‚
                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                         β”‚
                                         β”‚ (3) Process Payment
                                         β–Ό
                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                          β”‚         Bank / UPI Network    β”‚
                          β”‚   Processes & sends result    β”‚
                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                         β”‚
                                         β”‚ (4) Webhook
                                         β–Ό
                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                          β”‚  Merchant Backend Webhook     β”‚
                          β”‚ Updates DB, Publishes Kafka   β”‚
                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                         β”‚
                                         β”‚ (5) User Reconnects
                                         β–Ό
                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                          β”‚         User Client           β”‚
                          β”‚  Fetches final payment state  β”‚
                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

🧩 Step 5: UML Sequence Diagram

sequenceDiagram
    participant User
    participant ClientApp
    participant MerchantBackend
    participant RazorpayAPI
    participant Bank
    participant WebhookHandler
    participant DB

    User->>ClientApp: Click "Pay Now"
    ClientApp->>MerchantBackend: POST /create-order
    MerchantBackend->>RazorpayAPI: Create Order (idempotent)
    RazorpayAPI-->>MerchantBackend: razorpayOrderId
    MerchantBackend-->>ClientApp: Send Order ID

    ClientApp->>RazorpayAPI: Start Payment via Checkout
    RazorpayAPI->>Bank: Process Payment
    Bank-->>RazorpayAPI: Success

    RazorpayAPI-->>WebhookHandler: POST /webhook
    WebhookHandler->>WebhookHandler: Verify signature
    WebhookHandler->>DB: Update order & payment
    WebhookHandler->>Kafka: Publish payment.captured event

    Note right of WebhookHandler: Happens even if<br>user is offline

    ClientApp-->>User: User reconnects later
    ClientApp->>MerchantBackend: GET /payment-status
    MerchantBackend->>DB: Query latest status
    DB-->>MerchantBackend: status = "captured"
    MerchantBackend-->>ClientApp: Send final confirmation
    ClientApp-->>User: Show "Payment Successful βœ…"
Enter fullscreen mode Exit fullscreen mode

🧱 Step 6: Suggested Tech Stack

Layer Recommended Tools
Frontend React / Angular / Flutter / Android SDK
Backend Node.js (Express), Spring Boot, or Django
Database PostgreSQL / MongoDB
Cache Redis (for idempotency + status caching)
Message Queue Kafka / RabbitMQ / AWS SQS
API Gateway Nginx / Kong / AWS API Gateway
Monitoring Prometheus + Grafana / ELK Stack
Security HMAC validation, HTTPS, JWT Auth

πŸ” Step 7: Handling Failures Gracefully

Scenario Solution
Client disconnects Webhook ensures backend gets final result
User retries β€œPay Now” Same order ID β†’ idempotency prevents double charge
Webhook fails Retries via Kafka / Dead Letter Queue
Bank timeout Razorpay retries safely using internal transaction queue
DB crash Atomic transaction + durable logs ensure replay recovery

πŸ”’ Step 8: Security and Compliance

  • All API traffic is over HTTPS / TLS 1.2+
  • HMAC-SHA256 signature validates webhook authenticity
  • No card or UPI info stored β€” PCI-DSS compliance
  • JWT tokens for client–merchant authentication
  • Vault/KMS for secret key rotation

🧠 Step 9: Final Takeaways

Even if your internet fails right after β€œPay Now”:

  1. Razorpay continues the transaction with your bank.
  2. The merchant’s backend receives final confirmation via server webhook.
  3. When you come back online, your app simply checks your order ID.
  4. Because of idempotency + event-driven design, there’s:
  • No duplicate charge
  • No missed confirmation
  • A fully auditable, consistent payment flow

Razorpays Reliability Recipe

Ingredient Role
Idempotency Keys Prevent double payments
Server-to-Server Webhooks Reliable final status
Atomic DB Updates Consistent state
Kafka/Redis Queues Guaranteed delivery
HMAC Signatures Secure verification
Retry + Backoff Policies Network fault recovery

πŸ“Š Final Summary

Event Trigger Ensures
Create Order User initiates payment Unique ID for idempotency
Payment Initiated Client connects to Razorpay Secure checkout session
Webhook Received Razorpay confirms with backend Reliable confirmation
Status Fetch User reconnects Final truth retrieval

βœ… In short:
Razorpay’s system is not β€œclient-dependent” β€” it’s server-driven, idempotent, and event-consistent.
That’s how your payment succeeds β€” even if your phone doesn’t.


More Details:

Get all articles related to system design
Hastag: SystemDesignWithZeeshanAli

systemdesignwithzeeshanali

Git: https://github.com/ZeeshanAli-0704/SystemDesignWithZeeshanAli

Top comments (0)