Skip to main content

Design a Payment Gateway / Aggregator

Problem Statement

Design a Payment Gateway / Aggregator system (similar to Stripe or Razorpay). The gateway is responsible for routing client checkout transactions to downstream acquiring banks based on success rates, handling transaction status lifecycles, and enforcing transaction idempotency to prevent duplicate charges.

Asked In Companies

Functional Requirements

  • Support transaction states: CREATED, PENDING, SUCCESSFUL, and FAILED.
  • Register multiple acquiring partner banks (e.g., HDFC, ICICI).
  • Implement dynamic routing: auto-select the acquiring bank with the highest historical success rate.
  • Prevent double-charging using **Idempotency Keys** (if a request with the same key arrives, return the existing transaction instead of reprocessing).
  • Ensure thread safety during concurrent payment submissions and status checks.

Objects Required

  • TransactionStatus (Enum mapping payment lifecycles)
  • Transaction (Data model tracking payment progress)
  • AcquirerBank (Interface defining payment gateway communication logic)
  • PaymentGateway (Core processor matching routes and enforcing idempotency)

TransactionStatus Enum & Transaction Class

The TransactionStatus maps valid lifecycle phases, and the Transaction class records execution metadata along with the active bank route details.


public enum TransactionStatus {
    CREATED,
    PENDING,
    SUCCESSFUL,
    FAILED
}

Let's create the Transaction class:


public class Transaction {
    private final String id;
    private final double amount;
    private final String currency;
    private final String idempotencyKey;
    private TransactionStatus status;
    private String bankName;

    public Transaction(String id, double amount, String currency, String idempotencyKey) {
        this.id = id;
        this.amount = amount;
        this.currency = currency;
        this.idempotencyKey = idempotencyKey;
        this.status = TransactionStatus.CREATED;
    }

    public String getId() { return id; }
    public double getAmount() { return amount; }
    public String getCurrency() { return currency; }
    public String getIdempotencyKey() { return idempotencyKey; }
    
    public synchronized TransactionStatus getStatus() { return status; }
    public synchronized void setStatus(TransactionStatus status) { this.status = status; }

    public synchronized String getBankName() { return bankName; }
    public synchronized void setBankName(String bankName) { this.bankName = bankName; }
}

The constructor sets initial properties and sets the transaction to the CREATED state. Status and bank fields are synchronized to ensure thread safety when modified by callback threads.


AcquirerBank Interface & Mock Implementations

The AcquirerBank interface abstracts downstream bank integrations. Each implementation declares its name and success metrics.


public interface AcquirerBank {
    boolean processPayment(Transaction transaction);
    double getSuccessRate();
    String getName();
}

Let's write two implementations: HdfcBank and IciciBank.


public class HdfcBank implements AcquirerBank {
    private final double successRate;

    public HdfcBank(double successRate) {
        this.successRate = successRate;
    }

    @Override
    public boolean processPayment(Transaction transaction) {
        // Mock execution. Succeed if random float is less than success rate.
        return Math.random() < successRate;
    }

    @Override
    public double getSuccessRate() { return successRate; }

    @Override
    public String getName() { return "HDFC_BANK"; }
}

The HdfcBank overrides processPayment() to simulate transactions using a random float comparator against the target success threshold.


public class IciciBank implements AcquirerBank {
    private final double successRate;

    public IciciBank(double successRate) {
        this.successRate = successRate;
    }

    @Override
    public boolean processPayment(Transaction transaction) {
        return Math.random() < successRate;
    }

    @Override
    public double getSuccessRate() { return successRate; }

    @Override
    public String getName() { return "ICICI_BANK"; }
}

The IciciBank functions identically, but allows the gateway to dynamically route requests based on its success metric.


PaymentGateway Class

The PaymentGateway class handles bank registrations, enforces request idempotency, and handles payment processing routing.


import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

public class PaymentGateway {
    private final List<AcquirerBank> banks;
    private final Map<String, Transaction> idempotencyMap;

    public PaymentGateway() {
        this.banks = new CopyOnWriteArrayList<>();
        this.idempotencyMap = new ConcurrentHashMap<>();
    }

    public void registerBank(AcquirerBank bank) {
        banks.add(bank);
        System.out.println("Registered Acquirer: " + bank.getName() + " with Success Rate: " + bank.getSuccessRate());
    }

    public Transaction processPayment(String transactionId, double amount, String currency, String idempotencyKey) {
        // Enforce Idempotency check first
        if (idempotencyMap.containsKey(idempotencyKey)) {
            System.out.println("Duplicate request detected for key: " + idempotencyKey + ". Returning cached transaction.");
            return idempotencyMap.get(idempotencyKey);
        }

        Transaction transaction = new Transaction(transactionId, amount, currency, idempotencyKey);
        idempotencyMap.put(idempotencyKey, transaction);

        AcquirerBank bestBank = selectBestBank();
        if (bestBank == null) {
            transaction.setStatus(TransactionStatus.FAILED);
            System.err.println("Transaction failed: No active acquiring banks available.");
            return transaction;
        }

        transaction.setBankName(bestBank.getName());
        transaction.setStatus(TransactionStatus.PENDING);
        
        System.out.println("Routing transaction " + transactionId + " to bank: " + bestBank.getName());
        boolean success = bestBank.processPayment(transaction);

        if (success) {
            transaction.setStatus(TransactionStatus.SUCCESSFUL);
            System.out.println("Transaction " + transactionId + " processed successfully by " + bestBank.getName());
        } else {
            transaction.setStatus(TransactionStatus.FAILED);
            System.out.println("Transaction " + transactionId + " failed at " + bestBank.getName());
        }

        return transaction;
    }

    private AcquirerBank selectBestBank() {
        if (banks.isEmpty()) return null;
        // Dynamic Routing: Find the registered bank with the highest success rate
        return banks.stream()
                .max(Comparator.comparingDouble(AcquirerBank::getSuccessRate))
                .orElse(null);
    }
}

Here is an explanation of the core operations in the PaymentGateway class:

  • The constructor initializes thread-safe list variables and maps using ConcurrentHashMap to handle concurrent requests safely.
  • processPayment() checks for duplicate calls using the idempotencyMap. If the key is new, it instantiates a transaction, registers it, identifies the optimal bank routing, routes the call, and monitors the success flags.
  • selectBestBank() uses stream max operations to select the bank instance with the highest success rate.

Main Driver Class

This class tests our Payment Gateway. It registers banks, submits payments, validates dynamic routing, and tests idempotency logic.


public class Main {
    public static void main(String[] args) {
        PaymentGateway gateway = new PaymentGateway();

        // Register two banks with different success rates
        gateway.registerBank(new HdfcBank(0.95)); // 95% success rate (Preferred)
        gateway.registerBank(new IciciBank(0.80)); // 80% success rate

        System.out.println("\n--- Processing Payment 1 ---");
        Transaction t1 = gateway.processPayment("TXN-101", 1500.00, "INR", "idem-key-101");
        System.out.println("Payment 1 status: " + t1.getStatus() + " via " + t1.getBankName());

        // Process another payment with the same idempotency key
        System.out.println("\n--- Processing Duplicate Payment (Same Idempotency Key) ---");
        Transaction t2 = gateway.processPayment("TXN-102", 1500.00, "INR", "idem-key-101");
        System.out.println("Duplicate payment status: " + t2.getStatus() + " (Returned Transaction ID: " + t2.getId() + ")");

        System.out.println("\n--- Processing Payment 2 (New Idempotency Key) ---");
        Transaction t3 = gateway.processPayment("TXN-202", 500.00, "USD", "idem-key-202");
        System.out.println("Payment 2 status: " + t3.getStatus() + " via " + t3.getBankName());
    }
}

The main() driver configures the environment, registers bank instances, verifies that the system prioritizes the higher-rate partner (HDFC), and validates the idempotency intercept triggers.


Class Diagram

TransactionStatusCREATEDPENDINGSUCCESSFULFAILEDTransactionid: Stringamount: doublecurrency: Stringstatus: TransactionStatusbankName: StringidempotencyKey: StringgetId(): StringgetAmount(): doublegetCurrency(): StringgetStatus(): TransactionStatussetStatus(status: TransactionStatus): voidgetBankName(): StringsetBankName(name: String): voidgetIdempotencyKey(): StringAcquirerBankprocessPayment(t: Transaction): booleangetSuccessRate(): doublegetName(): StringHdfcBanksuccessRate: doubleprocessPayment(t: Transaction): booleangetSuccessRate(): doublegetName(): StringIciciBanksuccessRate: doubleprocessPayment(t: Transaction): booleangetSuccessRate(): doublegetName(): StringPaymentGatewaybanks: List<AcquirerBank>idempotencyMap: Map<String, Transaction>registerBank(bank: AcquirerBank): voidprocessPayment(id: String, amount: double, currency: String, idempotencyKey: String): TransactionselectBestBank(): AcquirerBankMainmain(args: String[]): void11routes to1manycaches/manages1manyinstantiatesprocessesdrives

Also See

Comments

Popular posts from this blog

Designing a Parking Lot - Low Level Design

Problem Statement Design a parking lot that can handle vehicles entering and leaving while managing parking across multiple floors. Each vehicle should be assigned a suitable parking spot based on its type, and the spot should be freed once the vehicle exits. The design should also support generating a ticket at entry and optionally calculating the parking fee based on the duration of stay. Asked In Companies Amazon Google Microsoft Uber Walmart Flipkart Meta PayPal Oracle Salesforce Adobe Apple Intuit LinkedIn Atlassian Functional Requirements The design should support multiple vehicle types such as bikes, cars, and trucks A vehicle must be assigned a parking spot compatible with its type A parking spot cannot be assigned to more than one vehicle at a time The parking lot should support multiple levels (floors) The design should search and allocate an availa...

Most Frequently Asked Low Level Design(LLD) Interview Questions

Below are the curated list of most commonly asked Low Level Design (LLD) interview problems. Each problem includes a short description and a link to the complete solution with code and class diagrams. Design Parking Lot System The system should handle parking for different vehicle types such as bikes, cars, and trucks. It should manage slot allocation, availability tracking, and entry/exit flow. The design also ensures efficient usage of parking space under varying load conditions. View Solution Design Elevator / Lift System The system should support multiple elevators operating across floors with request handling logic. It focuses on scheduling algorithms to minimize wait time and optimize movement. It also manages direction control and concurrent floor requests. View Solution Design Movie Ticket Booking System The system should allow users to browse movies, select shows, and book seats. It handles seat ...

Software Design Patterns for LLD Interviews: A Complete Guide

Software Design Patterns for LLD Interviews: A Complete Guide In Software Development Engineer (SDE) interviews—especially for mid-level and senior roles—low-level design (LLD) rounds assess your ability to write clean, reusable, maintainable, and extensible code. The foundation of resolving these architectural challenges lies in the standard Gang of Four (GoF) Design Patterns. Rather than memorizing theoretical definitions, interviewers expect you to apply these patterns to real-world scenarios, identifying the trade-offs of each. Below is a comprehensive guide to the 12 most frequently asked design patterns in LLD interviews, categorized by their classification (Creational, Structural, and Behavioral). Each pattern contains a concrete, real-world Java implementation and a detailed breakdown of design decisions. Creational Design Patterns Creational design patterns deal with object creation mechanisms. They abstract the instantiation process, making a system independent of how...