payment_gateway_system.md

💳 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:

  1. Idempotency: "If a user taps 'Pay' twice, or their 4G connection drops and retries, will their bank account be debited twice?"
  2. Retry Discipline: "Do you blindly retry every failure, or do you only retry transient network glitches while immediately failing terminal errors like Insufficient Balance?"
  3. 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?"
  4. 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:

  1. Idempotency Contract: Does the client send a unique idempotencyKey with each request? If a duplicate arrives, do we return the cached response, and what if a duplicate arrives while the first is still processing?
  2. Retry Rules: Which bank failures are retryable? I assume transient network timeouts are retryable, while terminal errors like INSUFFICIENT_FUNDS or INVALID_PIN must never be retried.
  3. Retry Limit: Should we support a maximum retry count (e.g., 3 attempts)?
  4. Async Queuing: Are payments processed synchronously or queued for background processing (e.g., for slow bank flows or webhooks)?
  5. 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) │ └─────────────────────┘
  1. Idempotency Guarantee:

    • Every transaction request includes an idempotencyKey generated by the client.
    • If a request with the same idempotencyKey was 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 an IN_PROGRESS status to prevent concurrent bank charges.
  2. 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).
  3. Asynchronous Queuing Mechanism:

    • Support both direct sync payment and an async queue pipeline.
    • For async flows, the gateway immediately returns PENDING_ASYNC with a tracking ID and enqueues the job into an in-memory PaymentQueue.
    • 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 (PaymentStatus enum: 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 (BankErrorType enum: 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 from idempotencyKey to cached response.
  • Responsibility: Guarantees that no request fingerprint is ever billed twice.

6. RetryPolicy

  • Attributes:
    • maxRetries (int, default 3).
  • Responsibility: Determines whether a specific BankErrorType is retryable and tracks remaining retry budgets.

7. PaymentQueue

  • Attributes:
    • jobQueue (vector<PaymentRequest*> or queue<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 ..> PaymentResponse

Step 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. Our IdempotencyStore::find() hits the cache, sees the previous SUCCESS record, 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:
    bool isRetryable(BankErrorType error) { if (error == BankErrorType::INSUFFICIENT_FUNDS || error == BankErrorType::INVALID_PIN) { return false; // Terminal failure } return true; // Only network glitches & server down }
    The gateway aborts immediately on attempt 1, saving latency and avoiding bank penalty rates.

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 the IdempotencyStore, 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 PaymentQueue with PENDING_ASYNC status 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 abstract BankGateway interface with a polymorphic processTransaction() contract, the payment engine does not depend on any specific bank's proprietary API."

Step 7: Extensibility Without Breaking Existing Code

  1. Adding Dynamic Smart Routing:

    • Add a Router component 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, or IdempotencyStore.
  2. Adding Exponential Backoff:

    • Enhance RetryPolicy to calculate backoff delays (e.g., $100\text{ms} \to 200\text{ms} \to 400\text{ms}$) between retry attempts.
  3. Adding Merchant Webhooks:

    • When an async job completes in processNextAsyncPayment(), dispatch an HTTP POST callback to the merchant's webhook URL with the final PaymentResponse.

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:

  1. Zero Double-Debits: "Every transaction is bound to a client-supplied idempotencyKey. Our IdempotencyStore intercepts duplicate requests and serves cached receipts with zero downstream bank invocations."
  2. Surgical Retries: "We categorize bank failures using a RetryPolicy. Transient network timeouts trigger auto-retries up to 3 times, while terminal errors like INSUFFICIENT_FUNDS abort immediately without wasting bank API limits."
  3. Decoupled Asynchronous Queue: "Slow intent flows and webhooks are ingested into an in-memory PaymentQueue returning PENDING_ASYNC in $O(1)$ time, allowing workers to drain payments without freezing user-facing checkout."
  4. Clean Abstractions: "Polymorphic BankGateway interfaces decouple the orchestrator from specific bank switches, making multi-bank failover trivial to add."