Introduction
Imagine you're purchasing a product online. You click the "Pay Now" button, and due to a slow internet connection or impatience, you accidentally click it again.
A common question among developers is:
If two requests are sent, why doesn't the payment get processed twice?
The answer lies in a combination of frontend protection, backend safeguards, database design, and payment gateway architecture.
In this blog, we'll explore how modern e-commerce systems prevent duplicate payments and the system design principles behind it.
The Problem
Consider the following sequence:
- User clicks "Pay Now"
- Browser sends payment request
- User clicks again before the first request completes
- Browser sends another payment request
Without proper handling:
- Customer may be charged twice
- Two orders may be created
- Inventory may be deducted twice
- Refund processes become necessary
This is a critical financial issue that every payment system must prevent.
First Layer: Frontend Protection
The simplest protection happens in the UI.
Disable the Payment Button
After the first click:
const handlePayment = async () => {
setLoading(true);
await makePayment();
};
<button disabled={loading}>
Pay Now
</button>
What Happens?
- User clicks once
- Button becomes disabled
- Additional clicks are ignored
Why This Isn't Enough
Frontend validation can be bypassed:
- Browser refresh
- Multiple tabs
- Network retries
- Malicious requests
- Mobile app bugs
Therefore, the backend must still assume duplicate requests can arrive.
Second Layer: Idempotency Keys
This is the most important concept in payment systems.
What Is Idempotency?
An operation is idempotent if performing it multiple times produces the same result.
For payments:
Pay ₹100 once
Pay ₹100 again
Pay ₹100 again
Result:
Only one actual payment is processed.
Generating an Idempotency Key
When a payment request is initiated:
payment_id = "PAY_123456"
Request:
{
"paymentId": "PAY_123456",
"amount": 1000
}
The backend stores this key.
First Request
PAY_123456
Status: Processing
Payment starts.
Second Request
Backend receives:
PAY_123456
System checks:
SELECT * FROM payments
WHERE payment_id = 'PAY_123456';
Record already exists.
Instead of processing again:
Return existing response
No second payment occurs.
Architecture Flow
User
|
v
Frontend
|
v
API Gateway
|
v
Payment Service
|
+---- Check Idempotency Key
|
+---- Already Exists?
|
YES --> Return Previous Result
|
NO
|
v
Process Payment
|
v
Store Result
This pattern is used by almost every modern payment platform.
Database-Level Protection
Even if two requests reach the server simultaneously, the database provides another safety layer.
Unique Constraint
CREATE TABLE payments (
payment_id VARCHAR(100) UNIQUE,
amount DECIMAL(10,2)
);
Suppose two servers receive:
PAY_123456
at exactly the same moment.
The first insert succeeds:
INSERT INTO payments ...
The second insert fails:
Duplicate Key Exception
Thus, duplicate records cannot exist.
Handling Race Conditions
Consider a distributed environment:
Request A --> Server 1
Request B --> Server 2
Both arrive within milliseconds.
Without coordination:
Server 1 -> Process Payment
Server 2 -> Process Payment
Duplicate charge risk.
Distributed Locking
Many systems use Redis locks.
Flow
Acquire Lock
|
v
Process Payment
|
v
Release Lock
Example:
LOCK:PAY_123456
Server 1:
Lock Acquired
Server 2:
Lock Exists
Server 2 waits or returns existing result.
Payment Gateway Protection
Payment providers also implement idempotency.
Examples include:
- Stripe
- Razorpay
- PayPal
When sending a request:
POST /payments
Idempotency-Key: PAY_123456
Gateway stores:
PAY_123456
If another request arrives:
Same key detected
Gateway returns the original response instead of charging again.
This creates protection even if your application has a bug.
Real-World Payment Architecture
User
|
v
+----------------+
| Frontend App |
+----------------+
|
v
+----------------+
| API Gateway |
+----------------+
|
v
+----------------+
| Payment Service|
+----------------+
|
+-----------+------------+
| |
v v
Redis Lock Payment Database
| |
+-----------+------------+
|
v
Payment Gateway
|
v
Bank
Every layer contributes to preventing duplicate transactions.
Why Multiple Protection Layers Are Needed
A common misconception is:
"Disabling the button solves the problem."
In reality:
| Layer | Purpose |
|---|---|
| Frontend Disable | Prevent accidental clicks |
| API Idempotency | Prevent duplicate processing |
| Database Unique Key | Prevent duplicate records |
| Redis Lock | Prevent race conditions |
| Payment Gateway Idempotency | Prevent duplicate charges |
Payment systems rely on defense in depth, not a single safeguard.
Interview Perspective
A common System Design interview question is:
How would you prevent duplicate payment processing?
Expected answer:
- Disable button on frontend
- Generate unique payment/order ID
- Use idempotency keys
- Store payment status in database
- Add unique constraints
- Use distributed locking for concurrent requests
- Leverage payment gateway idempotency support
Discussing all layers demonstrates strong backend and distributed systems knowledge.
Key Takeaways
- Double-clicking a payment button can generate multiple requests.
- Frontend protection alone is insufficient.
- Idempotency keys are the primary mechanism for preventing duplicate payments.
- Databases enforce uniqueness through constraints.
- Redis locks handle concurrent requests across multiple servers.
- Payment gateways provide an additional safety layer.
- Modern payment systems use multiple layers of protection to guarantee that a customer is charged only once.
A successful payment architecture is not about stopping duplicate requests it is about ensuring duplicate requests produce only one financial transaction.
Top comments (0)