This content originally appeared on DEV Community and was authored by ZeeshanAli-0704
Table of Contents
- How Razorpay Ensures Your Payment Succeeds Even If Your Internet Drops After You Click “Pay Now”
The Real World Problem
Step 1: Identifying the Core Problem
Step 2: The Reliable Payment Flow — End to End
- 1⃣ Client → Merchant Backend (Create Order)
- 2⃣ Client → Razorpay Checkout
- 3⃣ Razorpay → Bank / UPI Network
- 4⃣ Razorpay → Merchant Backend (Webhook)
-
5⃣ Client Reconnects → Merchant Backend
Step 3: Key Engineering Concepts
Step 4: System Architecture Diagram
Step 5: UML Sequence Diagram
Step 6: Suggested Tech Stack
Step 7: Handling Failures Gracefully
Step 8: Security & Compliance
Step 9: Final Takeaways
TL;DR — Razorpay’s Reliability Recipe
Final Summary
More Details — #SystemDesignWithZeeshanAli
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.
1. 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"
}
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 });
});
What happens:
- Your backend creates an order in Razorpay.
- The unique
razorpayOrderId
ensures idempotency. - Status = “CREATED” is stored in the DB.
2. 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();
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.
3. 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.
4. 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"
}
}
}
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");
});
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.
5. Client Reconnects → Merchant Backend
When the user reopens the app:
GET /api/payments/status?orderId=ORD_12345
Backend:
app.get("/api/payments/status", async (req, res) => {
const order = await db.orders.findOne({ orderId: req.query.orderId });
res.json({ status: order.status });
});
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 │
└───────────────────────────────┘
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 ✅"
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 & 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”:
- Razorpay continues the transaction with your bank.
- The merchant’s backend receives final confirmation via server webhook.
- When you come back online, your app simply checks your order ID.
- Because of idempotency + event-driven design, there’s:
- No duplicate charge
- No missed confirmation
- A fully auditable, consistent payment flow
TL;DR — Razorpay’s 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
Git: https://github.com/ZeeshanAli-0704/SystemDesignWithZeeshanAli
This content originally appeared on DEV Community and was authored by ZeeshanAli-0704