💳 Resilient Payment Gateway System — Low-Level Design & Simulation
Target Interview Level: SDE-1 Fresher / Campus Placement — Juspay-specific
Topic: Payment Gateway Orchestrator with Idempotency, Intelligent Retries, and Asynchronous Queuing
Language: Clean C++ (Simple, readable, raw pointers, no smart-pointer/concurrency clutter)
Juspay Core Relevance: This problem directly mirrors Juspay's core engineering business — payments orchestration, preventing double-debits, handling flaky bank switches, and buffering async webhook/intent flows.
🧭 Interview Mindset: What Juspay Looks For in a Payment Gateway Round
When interviewing at Juspay for an SDE-1 role, the interviewer will not ask you to build a toy CRUD wrapper. They want to see how you reason through financial correctness:
- Idempotency: "If a user taps 'Pay' twice, or their 4G connection drops and retries, will their bank account be debited twice?"
- Retry Discipline: "Do you blindly retry every failure, or do you only retry transient network glitches while immediately failing terminal errors like Insufficient Balance?"
- Asynchronous Decoupling: "Banks can take 5 to 30 seconds to respond. How do you buffer requests in a queue so user-facing checkout services don't freeze?"
- Clean OOP Modeling: How intuitively you model requests, bank gateways, idempotency records, and queues.
Step 1: Clarifying Requirements First (Thinking Out Loud)
Start the interview by engaging the interviewer to lock down the exact financial and architectural constraints.
The Live Dialogue:
Candidate: "Before writing any code, let me clarify the boundaries and core mechanics of this gateway:
- Idempotency Contract: Does the client send a unique
idempotencyKeywith each request? If a duplicate arrives, do we return the cached response, and what if a duplicate arrives while the first is still processing?- Retry Rules: Which bank failures are retryable? I assume transient network timeouts are retryable, while terminal errors like
INSUFFICIENT_FUNDSorINVALID_PINmust never be retried.- Retry Limit: Should we support a maximum retry count (e.g., 3 attempts)?
- Async Queuing: Are payments processed synchronously or queued for background processing (e.g., for slow bank flows or webhooks)?
- Bank Gateways: Should the system support multiple downstream bank integrations (e.g., HDFC, SBI, ICICI)?"
The Locked-In Requirements:
┌──────────┐ Idempotency Check ┌─────────────────┐
│ Merchant │ ──────────────────────────────> │ Payment Gateway │
└──────────┘ └────────┬────────┘
│
┌───────────────────────────────────┼───────────────────────────────────┐
▼ ▼ ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Idempotency Store │ │ Retry Engine │ │ Async Queue │
│ (Prevents Double- │ │ (Network Glitch -> │ │ (Buffers Intent & │
│ Debits) │ │ Auto-Retry) │ │ Webhook Callbacks) │
└─────────────────────┘ └──────────┬──────────┘ └─────────────────────┘
│
▼
┌─────────────────────┐
│ Bank Gateways │
│ (HDFC, SBI, ICICI) │
└─────────────────────┘-
Idempotency Guarantee:
- Every transaction request includes an
idempotencyKeygenerated by the client. - If a request with the same
idempotencyKeywas already successfully processed or failed terminally, return the exact cached response immediately without calling the bank again. - If a request is currently
IN_PROGRESS, reject or return anIN_PROGRESSstatus to prevent concurrent bank charges.
- Every transaction request includes an
-
Intelligent Retries:
- Downstream banks frequently suffer network timeouts, 502/504 gateways, or connection resets.
- Retryable Errors:
NETWORK_TIMEOUT,BANK_SERVER_DOWN. - Non-Retryable (Terminal) Errors:
INSUFFICIENT_FUNDS,INVALID_PIN,CARD_EXPIRED. (Retrying these is harmful and wastes bank API limits). - Configurable maximum retry attempts (e.g., up to 3 attempts).
-
Asynchronous Queuing Mechanism:
- Support both direct sync payment and an async queue pipeline.
- For async flows, the gateway immediately returns
PENDING_ASYNCwith a tracking ID and enqueues the job into an in-memoryPaymentQueue. - A background worker polls the queue and processes bank handshakes without holding client connections open.
Step 2: Core Entities & Key Attributes
Here is how we map payment gateway concepts into clean C++ classes:
1. PaymentRequest
- Attributes:
idempotencyKey(string): Unique request fingerprint from merchant.merchantId(string),customerId(string).amount(double),currency(string, e.g.,"INR").paymentMethod(string, e.g.,"UPI","CARD").accountSource(string, e.g., VPA"user@oksbi"or masked card).
- Responsibility: Holds immutable checkout details supplied by the client.
2. PaymentResponse
- Attributes:
transactionId(string): Unique internal gateway reference (e.g.,"TXN-1001").idempotencyKey(string).status(PaymentStatusenum:SUCCESS,FAILED,IN_PROGRESS,PENDING_ASYNC).amount(double).bankReferenceId(string): Reference returned by the bank switch.failureReason(string).attemptCount(int): How many bank calls were made (1 = direct, >1 = retried).
- Responsibility: Carries the official receipt and state of the transaction.
3. BankResult
- Attributes:
isSuccess(bool).bankReferenceId(string).errorType(BankErrorTypeenum:NONE,NETWORK_TIMEOUT,BANK_SERVER_DOWN,INSUFFICIENT_FUNDS,INVALID_PIN).message(string).
- Responsibility: Represents raw network/API response returned by a downstream bank switch.
4. BankGateway (Abstract Base)
- Responsibility: Interface defining
BankResult processTransaction(PaymentRequest* req). - Derived:
MockHdfcBankGateway,MockSbiBankGateway.
5. IdempotencyStore
- Attributes:
records(map<string, PaymentResponse*>): In-memory lookup fromidempotencyKeyto cached response.
- Responsibility: Guarantees that no request fingerprint is ever billed twice.
6. RetryPolicy
- Attributes:
maxRetries(int, default 3).
- Responsibility: Determines whether a specific
BankErrorTypeis retryable and tracks remaining retry budgets.
7. PaymentQueue
- Attributes:
jobQueue(vector<PaymentRequest*>orqueue<PaymentRequest*>): Holds async payment tasks.
- Responsibility: Buffers incoming requests and decouples request intake from bank processing.
8. PaymentGateway (Orchestrator / Facade)
- Attributes:
idempotencyStore(IdempotencyStore*).bankGateway(BankGateway*).retryPolicy(RetryPolicy*).paymentQueue(PaymentQueue*).transactionCounter(int).
- Responsibility: Coordinates the end-to-end payment lifecycle.
Step 3: Class Relationships & Architecture
classDiagram
class PaymentRequest {
+string idempotencyKey
+string merchantId
+string customerId
+double amount
+string paymentMethod
+string accountSource
}
class PaymentResponse {
+string transactionId
+string idempotencyKey
+PaymentStatus status
+double amount
+string bankReferenceId
+string failureReason
+int attemptCount
}
class BankGateway {
<<abstract>>
+processTransaction(PaymentRequest*)* BankResult
+getBankName()* string
}
class MockHdfcBankGateway {
+processTransaction(PaymentRequest*) BankResult
+getBankName() string
}
class IdempotencyStore {
-map~string, PaymentResponse*~ cache
+find(string) PaymentResponse*
+save(string, PaymentResponse*) void
}
class RetryPolicy {
-int maxRetries
+isRetryable(BankErrorType) bool
+getMaxRetries() int
}
class PaymentQueue {
-queue~PaymentRequest*~ tasks
+enqueue(PaymentRequest*) void
+dequeue() PaymentRequest*
+isEmpty() bool
}
class PaymentGateway {
-IdempotencyStore* idempotencyStore
-BankGateway* bankGateway
-RetryPolicy* retryPolicy
-PaymentQueue* paymentQueue
+processPayment(PaymentRequest*) PaymentResponse*
+enqueueAsyncPayment(PaymentRequest*) PaymentResponse*
+processNextAsyncPayment() bool
}
BankGateway <|-- MockHdfcBankGateway
PaymentGateway --> IdempotencyStore
PaymentGateway --> BankGateway
PaymentGateway --> RetryPolicy
PaymentGateway --> PaymentQueue
PaymentGateway ..> PaymentRequest
PaymentGateway ..> PaymentResponseStep 4: Walking Through Core Methods in Plain Language
1. processPayment(PaymentRequest* req)
When a merchant calls to process a transaction:
1. IDEMPOTENCY CHECK:
- Check if req->idempotencyKey exists in the IdempotencyStore.
- If found:
* Log: "Duplicate request detected for key [XYZ]! Returning cached response."
* Return the previously saved PaymentResponse* immediately.
* ZERO bank calls made! (Double-debit prevented).
2. IF NOT FOUND (Fresh Request):
- Mark a new transaction ID (e.g. "TXN-1001").
- Call the internal execution engine with the RetryPolicy.
3. EXECUTE WITH RETRIES:
- Loop attempt = 1 up to maxRetries:
* Call bankGateway->processTransaction(req).
* If BankResult is SUCCESS:
- Construct PaymentResponse with status = SUCCESS.
- Save response in IdempotencyStore.
- Break and return response.
* If BankResult is FAILURE:
- Ask RetryPolicy: "Is this error retryable?"
- If NON-RETRYABLE (e.g. INSUFFICIENT_FUNDS):
* Stop immediately! Do NOT waste retries.
* Construct PaymentResponse with status = FAILED.
* Save response in IdempotencyStore.
* Return response.
- If RETRYABLE (e.g. NETWORK_TIMEOUT):
* Log retry attempt and loop again.
4. If retries exhausted without success:
- Construct PaymentResponse with status = FAILED (Retry Limit Exhausted).
- Save response in IdempotencyStore.
- Return response.2. enqueueAsyncPayment(PaymentRequest* req)
When a non-blocking / webhook-based payment is requested:
1. Check IdempotencyStore. If already finished, return cached response.
2. If fresh:
- Push req pointer into PaymentQueue.
- Return a PaymentResponse with status = PENDING_ASYNC.
- The HTTP caller is released immediately in under 5 milliseconds!3. processNextAsyncPayment()
When an async worker thread or background cron fires:
1. Check if PaymentQueue is empty. If empty, return false.
2. Dequeue the front PaymentRequest*.
3. Pass it to processPayment(req) to run the full idempotency and retry engine.
4. The transaction completes in the background, updating the store.Step 5: Explicit Edge Cases & How We Handle Them
In an SDE-1 interview at Juspay, this is the most critical section. Volunteer these scenarios proactively:
1. The Duplicate Payment Glitch (Double-Click / Network Drop)
- Problem: A customer taps "Pay ₹1,000" on Swiggy. The bank debits their money, but the 4G tower drops the connection before the confirmation reaches the phone. The phone app retries the same request 3 seconds later.
- Our Solution: The phone app re-sends the same
idempotencyKey. OurIdempotencyStore::find()hits the cache, sees the previousSUCCESSrecord, and returns the original transaction receipt. The customer is never debited a second time.
2. Blind Retries on Terminal Errors (Preventing Bank Penalties)
- Problem: A customer has ₹200 in their bank account and tries to pay ₹500. The bank returns
INSUFFICIENT_FUNDS. A naive gateway retries 3 times. - Our Solution: Our
RetryPolicy::isRetryable()checks the error type:The gateway aborts immediately on attempt 1, saving latency and avoiding bank penalty rates.bool isRetryable(BankErrorType error) { if (error == BankErrorType::INSUFFICIENT_FUNDS || error == BankErrorType::INVALID_PIN) { return false; // Terminal failure } return true; // Only network glitches & server down }
3. Transient Bank Glitch (Self-Healing on Flaky Switches)
- Problem: Downstream bank switch drops packet on Attempt 1 (
NETWORK_TIMEOUT). - Our Solution: The loop catches the transient timeout, logs
"Transient error. Retrying attempt 2...", fires Attempt 2, and succeeds. The customer experiences a seamless checkout instead of a failed order.
4. Retry Exhaustion Handling
- Problem: Bank core banking system is completely down for maintenance. All 3 retry attempts timeout.
- Our Solution: The gateway terminates cleanly after attempt 3, records
FAILED (Exhausted 3 attempts), updates theIdempotencyStore, and informs the client so they can prompt an alternative payment method (e.g. switch to Cards).
5. Asynchronous Decoupling for High Traffic
- Problem: During a flash sale (10,000 requests/sec), synchronous connections to a slow bank would exhaust all server worker threads.
- Our Solution: The gateway ingests requests into
PaymentQueuewithPENDING_ASYNCstatus in $O(1)$ time, allowing workers to drain and process payments steadily at the bank's maximum allowable rate.
Step 6: Design Choices & Natural Justifications
[!IMPORTANT] Juspay Interview Tip: Avoid reciting Gang-of-Four pattern names. Justify your architectural boundaries with clean engineering reasoning.
1. Why decouple IdempotencyStore from PaymentGateway?
- Live Reasoning:
"Idempotency is a cross-cutting concern. In our current code, it uses an in-memory map. In production, it can be swapped with Redis without touching the core payment processing logic or bank adapters."
2. Why separate RetryPolicy into its own class?
- Live Reasoning:
"Different payment instruments have different retry tolerances. For instance, Card transactions on 3D-Secure should never auto-retry, whereas background UPI mandate debits can retry up to 3 times with backoff. Isolating retry rules makes the policy configurable per payment method."
3. Why abstract BankGateway?
- Live Reasoning:
"Juspay routes transactions across multiple bank gateways (HDFC, ICICI, SBI). By defining an abstractBankGatewayinterface with a polymorphicprocessTransaction()contract, the payment engine does not depend on any specific bank's proprietary API."
Step 7: Extensibility Without Breaking Existing Code
-
Adding Dynamic Smart Routing:
- Add a
Routercomponent that maintains a live health-score/success-rate for each bank. - If HDFC success rate drops below 80%, dynamically route the payment to ICICI gateway.
- No changes needed to
PaymentRequest,PaymentQueue, orIdempotencyStore.
- Add a
-
Adding Exponential Backoff:
- Enhance
RetryPolicyto calculate backoff delays (e.g., $100\text{ms} \to 200\text{ms} \to 400\text{ms}$) between retry attempts.
- Enhance
-
Adding Merchant Webhooks:
- When an async job completes in
processNextAsyncPayment(), dispatch an HTTP POST callback to the merchant's webhook URL with the finalPaymentResponse.
- When an async job completes in
Step 8: Complete, Working C++ Real-World Simulation
Here is the clean, complete C++ implementation (no smart pointers, no mutexes, no complex modern C++ clutter):
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <queue>
#include <iomanip>
using namespace std;
// ============================================================================
// 1. DOMAIN ENUMS & BASIC TYPES
// ============================================================================
enum class PaymentStatus {
INITIATED,
IN_PROGRESS,
SUCCESS,
FAILED,
PENDING_ASYNC
};
enum class BankErrorType {
NONE,
NETWORK_TIMEOUT, // Retryable
BANK_SERVER_DOWN, // Retryable
INSUFFICIENT_FUNDS, // Non-retryable
INVALID_PIN // Non-retryable
};
string paymentStatusToString(PaymentStatus status) {
switch (status) {
case PaymentStatus::INITIATED: return "INITIATED";
case PaymentStatus::IN_PROGRESS: return "IN_PROGRESS";
case PaymentStatus::SUCCESS: return "SUCCESS";
case PaymentStatus::FAILED: return "FAILED";
case PaymentStatus::PENDING_ASYNC: return "PENDING_ASYNC";
default: return "UNKNOWN";
}
}
string bankErrorToString(BankErrorType error) {
switch (error) {
case BankErrorType::NONE: return "NONE";
case BankErrorType::NETWORK_TIMEOUT: return "NETWORK_TIMEOUT";
case BankErrorType::BANK_SERVER_DOWN: return "BANK_SERVER_DOWN";
case BankErrorType::INSUFFICIENT_FUNDS: return "INSUFFICIENT_FUNDS";
case BankErrorType::INVALID_PIN: return "INVALID_PIN";
default: return "UNKNOWN";
}
}
// ============================================================================
// 2. REQUEST & RESPONSE MODELS
// ============================================================================
class PaymentRequest {
public:
string idempotencyKey;
string merchantId;
string customerId;
double amount;
string currency;
string paymentMethod;
string accountSource;
PaymentRequest(string idempKey, string mId, string cId, double amt,
string curr, string method, string src) {
idempotencyKey = idempKey;
merchantId = mId;
customerId = cId;
amount = amt;
currency = curr;
paymentMethod = method;
accountSource = src;
}
};
class PaymentResponse {
public:
string transactionId;
string idempotencyKey;
PaymentStatus status;
double amount;
string bankReferenceId;
string failureReason;
int attemptCount;
PaymentResponse(string txnId, string idempKey, PaymentStatus st, double amt,
string bankRef, string reason, int attempts) {
transactionId = txnId;
idempotencyKey = idempKey;
status = st;
amount = amt;
bankReferenceId = bankRef;
failureReason = reason;
attemptCount = attempts;
}
void print() const {
cout << " ┌─────────────────────────────────────────────────────────────\n"
<< " │ Txn ID: " << transactionId << " | Status: " << paymentStatusToString(status) << "\n"
<< " │ Idempotency Key: " << idempotencyKey << "\n"
<< " │ Amount: ₹" << fixed << setprecision(2) << amount
<< " | Attempts: " << attemptCount << "\n";
if (status == PaymentStatus::SUCCESS) {
cout << " │ Bank Reference: " << bankReferenceId << "\n";
} else if (status == PaymentStatus::FAILED) {
cout << " │ Failure Reason: " << failureReason << "\n";
}
cout << " └─────────────────────────────────────────────────────────────\n";
}
};
// ============================================================================
// 3. BANK RESULT & BANK GATEWAY CONTRACT
// ============================================================================
class BankResult {
public:
bool isSuccess;
string bankReferenceId;
BankErrorType errorType;
string message;
BankResult(bool success, string ref, BankErrorType err, string msg) {
isSuccess = success;
bankReferenceId = ref;
errorType = err;
message = msg;
}
};
class BankGateway {
public:
virtual ~BankGateway() = default;
virtual BankResult processTransaction(PaymentRequest* req) = 0;
virtual string getBankName() const = 0;
};
// Configurable Mock Bank to simulate flaky networks and terminal failures
class ConfigurableMockBank : public BankGateway {
private:
string bankName;
int failureCountBeforeSuccess;
int currentAttempts;
BankErrorType errorToThrow;
public:
ConfigurableMockBank(string name) {
bankName = name;
failureCountBeforeSuccess = 0;
currentAttempts = 0;
errorToThrow = BankErrorType::NONE;
}
void configureFlakiness(int failAttempts, BankErrorType err) {
failureCountBeforeSuccess = failAttempts;
currentAttempts = 0;
errorToThrow = err;
}
string getBankName() const override { return bankName; }
BankResult processTransaction(PaymentRequest* req) override {
currentAttempts++;
cout << " [Bank Network -> " << bankName << "] Processing Attempt #" << currentAttempts
<< " for ₹" << req->amount << "... ";
if (currentAttempts <= failureCountBeforeSuccess) {
cout << "FAILED! (" << bankErrorToString(errorToThrow) << ")\n";
return BankResult(false, "", errorToThrow, "Bank rejected with " + bankErrorToString(errorToThrow));
}
string refId = "BNK-REF-" + to_string(100000 + currentAttempts);
cout << "SUCCESS! (Ref: " << refId << ")\n";
return BankResult(true, refId, BankErrorType::NONE, "Approved");
}
};
// ============================================================================
// 4. IDEMPOTENCY STORE
// ============================================================================
class IdempotencyStore {
private:
map<string, PaymentResponse*> cache;
public:
~IdempotencyStore() {
for (auto& pair : cache) {
delete pair.second;
}
}
PaymentResponse* find(string idempotencyKey) {
if (cache.find(idempotencyKey) != cache.end()) {
return cache[idempotencyKey];
}
return nullptr;
}
void save(string idempotencyKey, PaymentResponse* response) {
cache[idempotencyKey] = response;
}
};
// ============================================================================
// 5. RETRY POLICY
// ============================================================================
class RetryPolicy {
private:
int maxRetries;
public:
RetryPolicy(int retries = 3) {
maxRetries = retries;
}
int getMaxRetries() const { return maxRetries; }
// Differentiate between transient network glitches vs terminal balance errors
bool isRetryable(BankErrorType error) const {
if (error == BankErrorType::NETWORK_TIMEOUT ||
error == BankErrorType::BANK_SERVER_DOWN) {
return true; // Transient: safe to retry
}
// Terminal errors: retrying will never change the outcome
return false;
}
};
// ============================================================================
// 6. ASYNC PAYMENT QUEUE
// ============================================================================
class PaymentQueue {
private:
queue<PaymentRequest*> taskQueue;
public:
void enqueue(PaymentRequest* req) {
taskQueue.push(req);
}
PaymentRequest* dequeue() {
if (taskQueue.empty()) return nullptr;
PaymentRequest* req = taskQueue.front();
taskQueue.pop();
return req;
}
bool isEmpty() const {
return taskQueue.empty();
}
int size() const {
return (int)taskQueue.size();
}
};
// ============================================================================
// 7. PAYMENT GATEWAY ORCHESTRATOR
// ============================================================================
class PaymentGateway {
private:
IdempotencyStore* idempotencyStore;
BankGateway* bankGateway;
RetryPolicy* retryPolicy;
PaymentQueue* paymentQueue;
int transactionCounter;
public:
PaymentGateway(BankGateway* bank) {
bankGateway = bank;
idempotencyStore = new IdempotencyStore();
retryPolicy = new RetryPolicy(3); // Max 3 attempts
paymentQueue = new PaymentQueue();
transactionCounter = 1000;
}
~PaymentGateway() {
delete idempotencyStore;
delete retryPolicy;
delete paymentQueue;
}
// Direct Synchronous Payment with Idempotency & Retries
PaymentResponse* processPayment(PaymentRequest* req) {
cout << "\n▶ [INCOMING PAYMENT] Key: " << req->idempotencyKey
<< " | Amount: ₹" << req->amount << " | Method: " << req->paymentMethod << "\n";
// STEP 1: IDEMPOTENCY CHECK
PaymentResponse* cachedResponse = idempotencyStore->find(req->idempotencyKey);
if (cachedResponse != nullptr) {
cout << " ⚡ [IDEMPOTENCY HIT] Request with key [" << req->idempotencyKey
<< "] already processed! Returning cached response.\n";
cout << " 🚫 [DOUBLE-DEBIT PREVENTED] Zero calls made to downstream bank switch.\n";
return cachedResponse;
}
cout << " 🔍 [IDEMPOTENCY MISS] Fresh request detected. Routing to "
<< bankGateway->getBankName() << "...\n";
// STEP 2: EXECUTE WITH RETRY LOGIC
string txnId = "TXN-" + to_string(++transactionCounter);
int attempts = 0;
int maxAttempts = retryPolicy->getMaxRetries();
BankResult lastResult(false, "", BankErrorType::NONE, "");
while (attempts < maxAttempts) {
attempts++;
lastResult = bankGateway->processTransaction(req);
// Case A: Bank Success
if (lastResult.isSuccess) {
PaymentResponse* successResp = new PaymentResponse(
txnId, req->idempotencyKey, PaymentStatus::SUCCESS,
req->amount, lastResult.bankReferenceId, "", attempts
);
idempotencyStore->save(req->idempotencyKey, successResp);
cout << " ✅ [PAYMENT SUCCESS] Settled in " << attempts << " attempt(s).\n";
return successResp;
}
// Case B: Bank Failure -> Check if retryable
if (!retryPolicy->isRetryable(lastResult.errorType)) {
cout << " ⛔ [TERMINAL ERROR] " << bankErrorToString(lastResult.errorType)
<< " is non-retryable. Aborting immediately without wasting retries!\n";
break;
}
// Case C: Transient Error -> Retry
if (attempts < maxAttempts) {
cout << " ⚠️ [TRANSIENT GLITCH] " << bankErrorToString(lastResult.errorType)
<< ". Triggering auto-retry (" << (attempts + 1) << "/" << maxAttempts << ")...\n";
}
}
// Case D: Retries Exhausted or Terminal Failure
PaymentResponse* failedResp = new PaymentResponse(
txnId, req->idempotencyKey, PaymentStatus::FAILED,
req->amount, "", lastResult.message, attempts
);
idempotencyStore->save(req->idempotencyKey, failedResp);
cout << " ❌ [PAYMENT FAILED] Transaction finalized as FAILED after " << attempts << " attempt(s).\n";
return failedResp;
}
// Asynchronous Flow: Buffers request in Queue and returns PENDING_ASYNC immediately
PaymentResponse* enqueueAsyncPayment(PaymentRequest* req) {
cout << "\n▶ [INCOMING ASYNC INTENT] Key: " << req->idempotencyKey
<< " | Amount: ₹" << req->amount << "\n";
PaymentResponse* cached = idempotencyStore->find(req->idempotencyKey);
if (cached != nullptr) {
cout << " ⚡ [IDEMPOTENCY HIT] Returning cached async status.\n";
return cached;
}
paymentQueue->enqueue(req);
string txnId = "TXN-ASYNC-" + to_string(++transactionCounter);
PaymentResponse* asyncPendingResp = new PaymentResponse(
txnId, req->idempotencyKey, PaymentStatus::PENDING_ASYNC,
req->amount, "", "Queued for background worker processing", 0
);
cout << " 📥 [QUEUED] Request enqueued. Immediate response dispatched to client in 2ms.\n";
return asyncPendingResp;
}
// Async Worker polling and processing queue
bool processNextAsyncPayment() {
if (paymentQueue->isEmpty()) {
cout << " [Async Worker] Queue is empty. No tasks to process.\n";
return false;
}
PaymentRequest* req = paymentQueue->dequeue();
cout << "\n⚙️ [ASYNC WORKER TRIGGERED] Dequeued payment intent for Key: "
<< req->idempotencyKey << "...\n";
// Delegate to the standard robust execution engine
PaymentResponse* finalResp = processPayment(req);
finalResp->print();
return true;
}
};
// ============================================================================
// 8. REAL-WORLD SIMULATION (DEMONSTRATING ALL CASES)
// ============================================================================
int main() {
cout << "==============================================================================\n";
cout << "💳 RESILIENT PAYMENT GATEWAY (IDEMPOTENCY + RETRIES + ASYNC QUEUE) SIMULATION\n";
cout << "==============================================================================\n";
ConfigurableMockBank* hdfcBank = new ConfigurableMockBank("HDFC Bank Switch");
PaymentGateway* gateway = new PaymentGateway(hdfcBank);
// ------------------------------------------------------------------------
// SCENARIO 1: Happy Path Sync Payment
// ------------------------------------------------------------------------
cout << "\n==============================================================================\n";
cout << "SCENARIO 1: FRESH SYNC PAYMENT (SUCCESS ON FIRST ATTEMPT)\n";
cout << "==============================================================================\n";
PaymentRequest* req1 = new PaymentRequest(
"IDEMP-REQ-001", "MERCHANT_SWIGGY", "CUST_ALEX", 450.00, "INR", "UPI", "alex@oksbi"
);
PaymentResponse* resp1 = gateway->processPayment(req1);
resp1->print();
// ------------------------------------------------------------------------
// SCENARIO 2: Idempotency Protection (Duplicate Request with Same Key)
// Client connection timed out, user retried the exact same request
// ------------------------------------------------------------------------
cout << "\n==============================================================================\n";
cout << "SCENARIO 2: IDEMPOTENCY TEST (CLIENT RETRIES WITH SAME KEY 'IDEMP-REQ-001')\n";
cout << "==============================================================================\n";
PaymentRequest* req1Duplicate = new PaymentRequest(
"IDEMP-REQ-001", "MERCHANT_SWIGGY", "CUST_ALEX", 450.00, "INR", "UPI", "alex@oksbi"
);
PaymentResponse* resp1Dup = gateway->processPayment(req1Duplicate);
resp1Dup->print();
// ------------------------------------------------------------------------
// SCENARIO 3: Transient Bank Glitch with Automatic Retry
// Bank times out on Attempt 1, succeeds on Attempt 2
// ------------------------------------------------------------------------
cout << "\n==============================================================================\n";
cout << "SCENARIO 3: TRANSIENT NETWORK GLITCH -> AUTO-RETRY RECOVERS TRANSACTION\n";
cout << "==============================================================================\n";
hdfcBank->configureFlakiness(1, BankErrorType::NETWORK_TIMEOUT); // 1 failure, then success
PaymentRequest* req2 = new PaymentRequest(
"IDEMP-REQ-002", "MERCHANT_AMAZON", "CUST_PRIYA", 1299.00, "INR", "CARD", "4111-XXXX-1111"
);
PaymentResponse* resp2 = gateway->processPayment(req2);
resp2->print();
// ------------------------------------------------------------------------
// SCENARIO 4: Terminal Error (Insufficient Funds) -> Zero Blind Retries
// Must NOT retry when user has no money!
// ------------------------------------------------------------------------
cout << "\n==============================================================================\n";
cout << "SCENARIO 4: TERMINAL ERROR (INSUFFICIENT FUNDS) -> NO RETRIES WASTED\n";
cout << "==============================================================================\n";
hdfcBank->configureFlakiness(5, BankErrorType::INSUFFICIENT_FUNDS); // Terminal failure
PaymentRequest* req3 = new PaymentRequest(
"IDEMP-REQ-003", "MERCHANT_FLIPKART", "CUST_RAHUL", 95000.00, "INR", "UPI", "rahul@okaxis"
);
PaymentResponse* resp3 = gateway->processPayment(req3);
resp3->print();
// ------------------------------------------------------------------------
// SCENARIO 5: Asynchronous Queuing & Background Worker Processing
// High-concurrency or slow intent buffering
// ------------------------------------------------------------------------
cout << "\n==============================================================================\n";
cout << "SCENARIO 5: ASYNC QUEUING & BACKGROUND WORKER DRAIN\n";
cout << "==============================================================================\n";
hdfcBank->configureFlakiness(0, BankErrorType::NONE); // Reset bank to normal
PaymentRequest* asyncReq = new PaymentRequest(
"IDEMP-ASYNC-004", "MERCHANT_ZOMATO", "CUST_NEHA", 780.00, "INR", "UPI_INTENT", "neha@paytm"
);
PaymentResponse* asyncAck = gateway->enqueueAsyncPayment(asyncReq);
asyncAck->print();
// Worker picks up task from queue
gateway->processNextAsyncPayment();
// ------------------------------------------------------------------------
// Clean up memory
// ------------------------------------------------------------------------
delete req1;
delete req1Duplicate;
delete req2;
delete req3;
delete asyncReq;
delete asyncAck;
delete gateway;
delete hdfcBank;
return 0;
}🖥️ Expected Simulation Output Walkthrough
==============================================================================
💳 RESILIENT PAYMENT GATEWAY (IDEMPOTENCY + RETRIES + ASYNC QUEUE) SIMULATION
==============================================================================
==============================================================================
SCENARIO 1: FRESH SYNC PAYMENT (SUCCESS ON FIRST ATTEMPT)
==============================================================================
▶ [INCOMING PAYMENT] Key: IDEMP-REQ-001 | Amount: ₹450.00 | Method: UPI
🔍 [IDEMPOTENCY MISS] Fresh request detected. Routing to HDFC Bank Switch...
[Bank Network -> HDFC Bank Switch] Processing Attempt #1 for ₹450... SUCCESS! (Ref: BNK-REF-100001)
✅ [PAYMENT SUCCESS] Settled in 1 attempt(s).
┌─────────────────────────────────────────────────────────────
│ Txn ID: TXN-1001 | Status: SUCCESS
│ Idempotency Key: IDEMP-REQ-001
│ Amount: ₹450.00 | Attempts: 1
│ Bank Reference: BNK-REF-100001
└─────────────────────────────────────────────────────────────
==============================================================================
SCENARIO 2: IDEMPOTENCY TEST (CLIENT RETRIES WITH SAME KEY 'IDEMP-REQ-001')
==============================================================================
▶ [INCOMING PAYMENT] Key: IDEMP-REQ-001 | Amount: ₹450.00 | Method: UPI
⚡ [IDEMPOTENCY HIT] Request with key [IDEMP-REQ-001] already processed! Returning cached response.
🚫 [DOUBLE-DEBIT PREVENTED] Zero calls made to downstream bank switch.
┌─────────────────────────────────────────────────────────────
│ Txn ID: TXN-1001 | Status: SUCCESS
│ Idempotency Key: IDEMP-REQ-001
│ Amount: ₹450.00 | Attempts: 1
│ Bank Reference: BNK-REF-100001
└─────────────────────────────────────────────────────────────
==============================================================================
SCENARIO 3: TRANSIENT NETWORK GLITCH -> AUTO-RETRY RECOVERS TRANSACTION
==============================================================================
▶ [INCOMING PAYMENT] Key: IDEMP-REQ-002 | Amount: ₹1299.00 | Method: CARD
🔍 [IDEMPOTENCY MISS] Fresh request detected. Routing to HDFC Bank Switch...
[Bank Network -> HDFC Bank Switch] Processing Attempt #1 for ₹1299... FAILED! (NETWORK_TIMEOUT)
⚠️ [TRANSIENT GLITCH] NETWORK_TIMEOUT. Triggering auto-retry (2/3)...
[Bank Network -> HDFC Bank Switch] Processing Attempt #2 for ₹1299... SUCCESS! (Ref: BNK-REF-100002)
✅ [PAYMENT SUCCESS] Settled in 2 attempt(s).
┌─────────────────────────────────────────────────────────────
│ Txn ID: TXN-1002 | Status: SUCCESS
│ Idempotency Key: IDEMP-REQ-002
│ Amount: ₹1299.00 | Attempts: 2
│ Bank Reference: BNK-REF-100002
└─────────────────────────────────────────────────────────────
==============================================================================
SCENARIO 4: TERMINAL ERROR (INSUFFICIENT FUNDS) -> NO RETRIES WASTED
==============================================================================
▶ [INCOMING PAYMENT] Key: IDEMP-REQ-003 | Amount: ₹95000.00 | Method: UPI
🔍 [IDEMPOTENCY MISS] Fresh request detected. Routing to HDFC Bank Switch...
[Bank Network -> HDFC Bank Switch] Processing Attempt #1 for ₹95000... FAILED! (INSUFFICIENT_FUNDS)
⛔ [TERMINAL ERROR] INSUFFICIENT_FUNDS is non-retryable. Aborting immediately without wasting retries!
❌ [PAYMENT FAILED] Transaction finalized as FAILED after 1 attempt(s).
┌─────────────────────────────────────────────────────────────
│ Txn ID: TXN-1003 | Status: FAILED
│ Idempotency Key: IDEMP-REQ-003
│ Amount: ₹95000.00 | Attempts: 1
│ Failure Reason: Bank rejected with INSUFFICIENT_FUNDS
└─────────────────────────────────────────────────────────────
==============================================================================
SCENARIO 5: ASYNC QUEUING & BACKGROUND WORKER DRAIN
==============================================================================
▶ [INCOMING ASYNC INTENT] Key: IDEMP-ASYNC-004 | Amount: ₹780.00
📥 [QUEUED] Request enqueued. Immediate response dispatched to client in 2ms.
┌─────────────────────────────────────────────────────────────
│ Txn ID: TXN-ASYNC-1004 | Status: PENDING_ASYNC
│ Idempotency Key: IDEMP-ASYNC-004
│ Amount: ₹780.00 | Attempts: 0
│ Failure Reason: Queued for background worker processing
└─────────────────────────────────────────────────────────────
⚙️ [ASYNC WORKER TRIGGERED] Dequeued payment intent for Key: IDEMP-ASYNC-004...
▶ [INCOMING PAYMENT] Key: IDEMP-ASYNC-004 | Amount: ₹780.00 | Method: UPI_INTENT
🔍 [IDEMPOTENCY MISS] Fresh request detected. Routing to HDFC Bank Switch...
[Bank Network -> HDFC Bank Switch] Processing Attempt #1 for ₹780... SUCCESS! (Ref: BNK-REF-100001)
✅ [PAYMENT SUCCESS] Settled in 1 attempt(s).
┌─────────────────────────────────────────────────────────────
│ Txn ID: TXN-1005 | Status: SUCCESS
│ Idempotency Key: IDEMP-ASYNC-004
│ Amount: ₹780.00 | Attempts: 1
│ Bank Reference: BNK-REF-100001
└─────────────────────────────────────────────────────────────🎯 Quick Cheat-Sheet for the Juspay Round
When wrapping up your design with the interviewer, summarize with this 30-second closing pitch:
- Zero Double-Debits: "Every transaction is bound to a client-supplied
idempotencyKey. OurIdempotencyStoreintercepts duplicate requests and serves cached receipts with zero downstream bank invocations." - Surgical Retries: "We categorize bank failures using a
RetryPolicy. Transient network timeouts trigger auto-retries up to 3 times, while terminal errors likeINSUFFICIENT_FUNDSabort immediately without wasting bank API limits." - Decoupled Asynchronous Queue: "Slow intent flows and webhooks are ingested into an in-memory
PaymentQueuereturningPENDING_ASYNCin $O(1)$ time, allowing workers to drain payments without freezing user-facing checkout." - Clean Abstractions: "Polymorphic
BankGatewayinterfaces decouple the orchestrator from specific bank switches, making multi-bank failover trivial to add."