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.
Functional Requirements
- Support transaction states:
CREATED,PENDING,SUCCESSFUL, andFAILED. - 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
ConcurrentHashMapto handle concurrent requests safely. processPayment()checks for duplicate calls using theidempotencyMap. 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.
Comments
Post a Comment