Build Payment Gateway System | Low-Level Design (LLD)
This comprehensive study guide covers the low-level system design of a modular Payment Gateway, designed using clean object-oriented principles, design patterns, and robust architectural workflows.
1. Why Do We Need This? (Problem Statement & Scope)
In modern software engineering, almost every consumer application—such as Amazon, Zomato, Ola, Uber, or Swiggy—requires a mechanism to process user payments securely and reliably.
- The Problem: Building payment processing directly inside a food-delivery or e-commerce app creates tight coupling with specific financial vendors. If a new payment provider emerges, refactoring core application logic is tedious and error-prone.
- The Goal: Build an extensible, plug-and-play Payment Gateway Integration Layer. This intermediary system acts as a standard abstraction layer between any client application (e.g., Amazon) and multiple external payment providers (e.g., Paytm, Razorpay), shielding the client from the underlying complexities of banking networks.
- Scope Clarification: This is not an internal core banking ledger (such as the internal fund-management ledger of Paytm or Google Pay). Instead, this is a plug-and-play gateway wrapper that any client application can integrate to delegate payment execution to third-party processors.
2. Core Mental Model & Analogies
Real-Life Analogy: The Universal Power Adapter
Imagine traveling internationally where every country has a different wall socket shape. Instead of rewiring your appliances for every country, you carry a Universal Travel Adapter.
- Your application appliance plugs into the adapter (the Payment Gateway).
- The adapter handles the specific shapes and voltages of different wall outlets (Paytm, Razorpay).
- Your appliance doesn't need to know how electricity is generated or delivered from the local power plant (Banking System); it just communicates through a standard interface.
3. Functional Requirements & Design Constraints
To design a production-grade payment gateway, we must satisfy specific functional expectations:
- Multiple Provider Support: Must seamlessly support existing providers like Paytm and Razorpay, with the architectural flexibility to plug in new ones (e.g., Google Pay, PayPal) without modifying existing code (Open-Closed Principle).
- Standardized Payment Flow: Every transaction must follow an unvarying, sequential workflow: Validate -> Initiate -> Confirm.
- Error Handling & Retry Mechanisms: Network drops, timeouts, and insufficient funds are inevitable. The system must automatically retry failed transactions based on configurable policies.
- Extensibility & Separation of Concerns: Isolating business rules, network proxies, and third-party bank communication into distinct classes.
4. Naive vs. Better Approach
5. Step-by-Step Architectural Evolution
Step 1: The Request Model (PaymentRequest)
We start by defining a simple data model containing all immutable and mutable details required to execute a transaction:
sender: Name/ID of the user paying.receiver: Name/ID of the merchant or recipient.amount: Numerical value of the transaction (double).currency: ISO string representation (e.g.,INR,USD) to support international gateways like PayPal.
Step 2: The Banking System Hierarchy (IBankingSystem)
External banks operate outside our application boundary over HTTP/Internet. We abstract this using an interface:
Step 3: The Template Method Pattern for Standardized Flows
To guarantee that every payment follows the exact sequence—Validate Initiate Confirm—we employ the Template Method Design Pattern.
6. Detailed UML & Component Breakdowns
Let's examine how our components link together using clean object-oriented compositions and associations.
Key Design Patterns Applied:
- Template Method Pattern: Enforces the rigid Validate Initiate Confirm execution pipeline inside
PaymentGateway.processPayment(). - Factory Pattern (
GatewayFactory): Encapsulates object creation. Based on aGatewayTypeenum (Paytm,Razorpay), it instantiates the appropriate gateway wrapped inside a proxy. - Proxy Pattern (
PaymentGatewayProxy): Implements the protection/virtual proxy concept. It intercepts calls to the real payment gateway and wraps execution inside an automatic Retry Mechanism, keeping retry logic out of core business classes and client code. - Singleton Pattern: Applied to
GatewayFactory,PaymentService, andPaymentControllerto maintain single instances across the application lifecycle.
7. Complete Code Implementation (C++ / OOP Paradigm)
Below is the complete, high-fidelity codebase demonstrating our architecture, structured cleanly for production-level understanding.
#include <iostream>
#include <string>
#include <cstdlib>
#include <ctime>
using namespace std;
// ==========================================
// 1. PAYMENT REQUEST MODEL
// ==========================================
class PaymentRequest {
private:
string sender;
string receiver;
double amount;
string currency;
public:
PaymentRequest(string s, string r, double amt, string curr)
: sender(s), receiver(r), amount(amt), currency(curr) {}
string getSender() const { return sender; }
string getReceiver() const { return receiver; }
double getAmount() const { return amount; }
string getCurrency() const { return currency; }
};
// ==========================================
// 2. BANKING SYSTEM INTERFACE & HIERARCHY
// ==========================================
class IBankingSystem {
public:
virtual bool processPayment(double amount) = 0;
virtual ~IBankingSystem() {}
};
class PaytmBankingSystem : public IBankingSystem {
public:
bool processPayment(double amount) override {
// Simulating 80% success rate (20% failure)
int randomVal = rand() % 100;
return (randomVal < 80);
}
};
class RazorpayBankingSystem : public IBankingSystem {
public:
bool processPayment(double amount) override {
// Simulating 90% success rate (10% failure)
int randomVal = rand() % 100;
return (randomVal < 90);
}
};
// ==========================================
// 3. PAYMENT GATEWAY (TEMPLATE METHOD PATTERN)
// ==========================================
class IPaymentGateway {
public:
virtual bool processPayment(const PaymentRequest& request) = 0;
virtual bool validatePayment(const PaymentRequest& request) = 0;
virtual bool initiatePayment(const PaymentRequest& request) = 0;
virtual bool confirmPayment(const PaymentRequest& request) = 0;
virtual ~IPaymentGateway() {}
};
class PaymentGateway : public IPaymentGateway {
protected:
IBankingSystem* bankingSystem;
public:
PaymentGateway(IBankingSystem* bank) : bankingSystem(bank) {}
// Template Method enforcing strict sequential flow
bool processPayment(const PaymentRequest& request) override {
if (!validatePayment(request)) {
throw runtime_error("Validation Failed for transaction.");
}
if (!initiatePayment(request)) {
throw runtime_error("Initiation Failed for transaction.");
}
if (!confirmPayment(request)) {
throw runtime_error("Confirmation Failed for transaction.");
}
return true;
}
virtual ~PaymentGateway() {
delete bankingSystem;
}
};
// ==========================================
// 4. CONCRETE GATEWAYS
// ==========================================
class PaytmGateway : public PaymentGateway {
public:
PaytmGateway() : PaymentGateway(new PaytmBankingSystem()) {}
bool validatePayment(const PaymentRequest& request) override {
cout << "[Paytm] Validating payment for sender: " << request.getSender() << "\n";
if (request.getAmount() <= 0) return false;
if (request.getCurrency() != "INR") return false; // Paytm strictly INR here
return true;
}
bool initiatePayment(const PaymentRequest& request) override {
cout << "[Paytm] Initiating payment of " << request.getAmount() << " " << request.getCurrency() << "\n";
return bankingSystem->processPayment(request.getAmount());
}
bool confirmPayment(const PaymentRequest& request) override {
cout << "[Paytm] Confirming transaction successfully.\n";
return true;
}
};
class RazorpayGateway : public PaymentGateway {
public:
RazorpayGateway() : PaymentGateway(new RazorpayBankingSystem()) {}
bool validatePayment(const PaymentRequest& request) override {
cout << "[Razorpay] Validating payment for sender: " << request.getSender() << "\n";
if (request.getAmount() <= 0) return false;
return true; // Accepts multiple currencies
}
bool initiatePayment(const PaymentRequest& request) override {
cout << "[Razorpay] Initiating payment of " << request.getAmount() << " " << request.getCurrency() << "\n";
return bankingSystem->processPayment(request.getAmount());
}
bool confirmPayment(const PaymentRequest& request) override {
cout << "[Razorpay] Confirming transaction successfully.\n";
return true;
}
};
// ==========================================
// 5. PROXY PATTERN (RETRY & ERROR HANDLING)
// ==========================================
class PaymentGatewayProxy : public IPaymentGateway {
private:
IPaymentGateway* realGateway;
int maxRetries;
public:
PaymentGatewayProxy(IPaymentGateway* gateway, int retries)
: realGateway(gateway), maxRetries(retries) {}
bool processPayment(const PaymentRequest& request) override {
int attempt = 0;
bool success = false;
while (attempt < maxRetries && !success) {
attempt++;
cout << ">>> Attempt " << attempt << " of " << maxRetries << " for transaction.\n";
try {
success = realGateway->processPayment(request);
if (success) {
cout << ">>> Transaction Succeeded on attempt " << attempt << "!\n";
return true;
}
} catch (const exception& e) {
cout << ">>> Error caught: " << e.what() << " on attempt " << attempt << "\n";
}
}
cout << ">>> Transaction Failed after " << maxRetries << " attempts.\n";
return false;
}
bool validatePayment(const PaymentRequest& request) override {
return realGateway->validatePayment(request);
}
bool initiatePayment(const PaymentRequest& request) override {
return realGateway->initiatePayment(request);
}
bool confirmPayment(const PaymentRequest& request) override {
return realGateway->confirmPayment(request);
}
~PaymentGatewayProxy() {
delete realGateway;
}
};
// ==========================================
// 6. FACTORY PATTERN
// ==========================================
enum class GatewayType { Paytm, Razorpay };
class GatewayFactory {
private:
GatewayFactory() {}
public:
static GatewayFactory& getInstance() {
static GatewayFactory instance;
return instance;
}
IPaymentGateway* getGateway(GatewayType type) {
if (type == GatewayType::Paytm) {
// Wrap Paytm in Proxy allowing 3 retries
return new PaymentGatewayProxy(new PaytmGateway(), 3);
} else if (type == GatewayType::Razorpay) {
// Wrap Razorpay in Proxy allowing 2 retries
return new PaymentGatewayProxy(new RazorpayGateway(), 2);
}
return nullptr;
}
};
// ==========================================
// 7. SERVICE & CONTROLLER LAYERS
// ==========================================
class PaymentService {
private:
IPaymentGateway* activeGateway;
PaymentService() : activeGateway(nullptr) {}
public:
static PaymentService& getInstance() {
static PaymentService instance;
return instance;
}
void setGateway(IPaymentGateway* gateway) {
if (activeGateway) delete activeGateway;
activeGateway = gateway;
}
bool makePayment(const PaymentRequest& request) {
if (!activeGateway) {
throw runtime_error("No Payment Gateway configured!");
}
return activeGateway->processPayment(request);
}
};
class PaymentController {
private:
PaymentController() {}
public:
static PaymentController& getInstance() {
static PaymentController instance;
return instance;
}
bool handlePaymentRequest(GatewayType type, const PaymentRequest& request) {
// 1. Fetch Gateway from Factory (wrapped with Proxy)
IPaymentGateway* gateway = GatewayFactory::getInstance().getGateway(type);
// 2. Inject Gateway into Service layer
PaymentService::getInstance().setGateway(gateway);
// 3. Execute transaction flow
return PaymentService::getInstance().makePayment(request);
}
};
// ==========================================
// 8. MAIN DRIVER EXECUTION
// ==========================================
int main() {
srand(static_cast<unsigned int>(time(0)));
// Request 1: Paytm (INR)
PaymentRequest req1("Aditya", "Shubham", 1000.0, "INR");
cout << "--- Processing Payment via Paytm ---\n";
bool res1 = PaymentController::getInstance().handlePaymentRequest(GatewayType::Paytm, req1);
cout << "Result 1: " << (res1 ? "SUCCESS" : "FAILED") << "\n\n";
// Request 2: Razorpay (USD)
PaymentRequest req2("Shubham", "Aditya", 500.0, "USD");
cout << "--- Processing Payment via Razorpay ---\n";
bool res2 = PaymentController::getInstance().handlePaymentRequest(GatewayType::Razorpay, req2);
cout << "Result 2: " << (res2 ? "SUCCESS" : "FAILED") << "\n";
return {0};
}
8. Production Reality & Debugging Mindset
When deploying payment gateways to production environments, engineers frequently encounter specific failure modes:
- Network Timeouts & Idempotency: If a client sends a payment request and the connection drops before receiving a response, retrying the request blindly can result in double charging. Production systems require an Idempotency Key so that repeated requests with the same key return the previous cached response without re-triggering funds transfer.
- Cascading Failures: If Paytm's servers experience an outage, throwing immediate errors to users degrades the customer experience. Production gateways implement Circuit Breakers to gracefully fallback to alternative providers (e.g., failing over from Paytm to Razorpay automatically).
- Audit Logging: Every validation failure, initiation attempt, and confirmation status must be logged centrally with correlation IDs for financial reconciliation and debugging.
9. Summary and Key Takeaways
- Plug-and-Play Architecture: The system cleanly decouples client applications from external banking APIs by introducing a modular
PaymentGatewayabstraction layer. - Strict Workflow Order via Template Pattern: The
processPayment()template method guarantees that validation always precedes initiation, followed strictly by confirmation. - Separation of Concerns with Proxies: The Proxy Pattern isolates transient error handling and Retry Mechanisms away from core business logic classes.
- Extensible Creation: The Factory Pattern (
GatewayFactory) enables effortless integration of future payment providers (like Google Pay or Apple Pay) by simply registering new enums and concrete subclasses. - Scalable Foundation: Clean service and controller separation ensures that business logic remains clean, testable, and maintainable under enterprise workloads.