Designing a Payment Gateway is a core LLD topic at Razorpay, PayU, PhonePe, Paytm, and any fintech company. It requires understanding idempotency, multi-method payment processing, fraud detection, and webhook reliability. Here's a complete design.
Core Entities
- Payment — idempotencyKey, amount, currency, status, method
- Transaction — maps to a payment provider's transaction
- PaymentMethod — Card, UPI, NetBanking, Wallet
- Refund — references original Payment, amount, reason
- Webhook — event type, payload, delivery status
- FraudCheck — result of fraud detection pipeline
Idempotency — Most Critical Concept
class PaymentService {
processPayment(request: PaymentRequest): Payment {
// Check if this idempotency key was already processed
const existing = this.paymentRepo.findByIdempotencyKey(request.idempotencyKey);
if (existing) return existing; // Return same result, don't charge again
const payment = new Payment(request);
this.paymentRepo.save(payment);
// Process through fraud checks and payment provider
const fraudResult = this.fraudPipeline.check(request);
if (fraudResult.rejected) {
payment.fail("FRAUD_DETECTED");
return payment;
}
const result = this.getProvider(request.method).charge(payment);
payment.updateFromProviderResult(result);
this.webhookService.dispatch(payment);
return payment;
}
}Strategy for Payment Methods
interface PaymentProvider {
charge(payment: Payment): ProviderResult;
refund(transaction: Transaction, amount: number): RefundResult;
}
class RazorpayCardProvider implements PaymentProvider { ... }
class UPIProvider implements PaymentProvider { ... }
class WalletProvider implements PaymentProvider { ... }Chain of Responsibility for Fraud Detection
abstract class FraudCheck {
protected next: FraudCheck | null = null;
setNext(c: FraudCheck) { this.next = c; return c; }
abstract check(req: PaymentRequest): FraudResult;
}
class VelocityCheck extends FraudCheck {
check(req) {
const recent = this.txRepo.countInLastHour(req.userId);
if (recent > 10) return FraudResult.reject("TOO_MANY_ATTEMPTS");
return this.next?.check(req) ?? FraudResult.pass();
}
}
class AmountAnomalyCheck extends FraudCheck { ... }
class GeoMismatchCheck extends FraudCheck { ... }Webhook Reliability
class WebhookService {
dispatch(payment: Payment) {
const event = new WebhookEvent("payment.captured", payment);
const merchants = this.merchantRepo.getSubscribed(payment.merchantId);
merchants.forEach(m => this.deliverWithRetry(m.webhookUrl, event));
}
private async deliverWithRetry(url: string, event: WebhookEvent, attempt = 0) {
const success = await this.httpClient.post(url, event);
if (!success && attempt < 5)
setTimeout(() => this.deliverWithRetry(url, event, attempt + 1),
Math.pow(2, attempt) * 1000);
}
}