
Introduction
Payment Gateway is the heart of every FinTech platform — where money flows from buyer to seller is processed. In this article, we will design the complete Payment Gateway architecture, from checkout flow to settlement.
1. What is Payment Gateway?
1.1 Role in Payment Ecosystem
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Customer │───►│ Merchant │───►│ Payment │───►│ Acquirer │───►│ Issuer │
│ │ │ (Shop) │ │ Gateway │ │ Bank │ │ Bank │
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
│
│ Orchestration
│
┌──────▼──────┐
│ Payment │
│ Processors │
│ (PSPs) │
└─────────────┘
Payment Gateway plays the role of orchestrator — coordinating the payment flow between merchant, acquirer bank, card network, and issuer bank.
1.2 Payment Gateway vs Payment Processor
| Ingredients | Role | Example |
|---|---|---|
| Payment Gateway | Receive, validate, route payment requests | Stripe, VNPay |
| Payment Processor | Handling transactions with card networks | Worldpay, First Data |
| Acquirer | Merchant's bank, receive funds | Vietcombank, BIDV |
| Issuer | Bank issues cards to customers | Techcombank, ACB |
| Card Network | Network connection acquirer-issuer | Visa, Mastercard, NAPAS |
2. Payment Lifecycle
2.1 Payment statuses
┌───────────┐
│ CREATED │
└─────┬─────┘
│
┌─────▼─────┐
┌─────│ PENDING │─────┐
│ └─────┬─────┘ │
│ │ │
┌─────▼─────┐ │ ┌──────▼─────┐
│ FAILED │ │ │ EXPIRED │
└───────────┘ │ └────────────┘
│
┌─────▼──────┐
│ AUTHORIZED │
└─────┬──────┘
│
┌──────────┼──────────┐
│ │ │
┌─────▼─────┐ │ ┌─────▼─────┐
│ VOIDED │ │ │ CAPTURED │
└───────────┘ │ └─────┬─────┘
│ │
│ ┌─────▼─────┐
│ │ SETTLED │
│ └─────┬─────┘
│ │
│ ┌─────▼─────┐
└──►│ REFUNDED │
└───────────┘
2.2 Two-Phase Payment (Auth + Capture)
Phase 1: Authorization (Reserve funds)
──────────────────────────────────────
Customer ──► Gateway ──► PSP ──► Issuer Bank
│
Reserve $100
(Hold on card)
│
Customer ◄── Gateway ◄── PSP ◄─────┘
Auth Code: auth_xyz
Phase 2: Capture (Collect funds)
──────────────────────────────────────
Merchant ──► Gateway ──► PSP ──► Issuer Bank
│
Capture $100
(Move from hold)
│
Merchant ◄── Gateway ◄── PSP ◄─────┘
Captured: $100
Use cases for Two-Phase:
- Hotels (reserve when booking, capture when checking out)
- Marketplaces (captured upon shipping)
- Pre-orders
3. Payment Gateway Architecture
3.1 Internal Components
┌─────────────────────────────────────────────────────────────┐
│ PAYMENT GATEWAY │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ Checkout │ │ Payment │ │ Router │ │ PSP │ │
│ │ Service │ │ Engine │ │ Service │ │ Adapters │ │
│ └────┬──────┘ └────┬──────┘ └────┬──────┘ └────┬───────┘ │
│ │ │ │ │ │
│ ┌────▼──────────────▼──────────────▼──────────────▼───────┐ │
│ │ Event Bus (Kafka) │ │
│ └─────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌──────────┐ ┌──────────▼──┐ ┌──────────┐ ┌──────────┐ │
│ │Idempotency│ │ State │ │ Retry │ │ Webhook │ │
│ │ Store │ │ Machine │ │ Engine │ │ Sender │ │
│ └──────────┘ └─────────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
3.2 Payment Request Flow
// Simplified payment processing flow
public class PaymentEngine {
public PaymentResult processPayment(PaymentRequest request) {
// 1. Idempotency check
var existing = idempotencyStore.find(request.getIdempotencyKey());
if (existing.isPresent()) {
return existing.get(); // Return cached result
}
// 2. Validate request
validator.validate(request);
// 3. Create payment record
var payment = Payment.create(request);
paymentRepository.save(payment);
// 4. Risk check
var riskResult = riskService.evaluate(payment);
if (riskResult.isBlocked()) {
payment.fail(riskResult.getReason());
return PaymentResult.blocked(riskResult);
}
// 5. Route to PSP
var psp = router.selectPSP(payment);
var adapter = pspAdapterFactory.getAdapter(psp);
// 6. Process with PSP
try {
var pspResult = adapter.authorize(payment);
payment.authorize(pspResult);
// 7. Publish event
eventBus.publish(new PaymentAuthorizedEvent(payment));
return PaymentResult.success(payment);
} catch (PSPException e) {
payment.fail(e.getMessage());
return PaymentResult.failed(e);
}
}
}
3.3 Idempotency Pattern
Request 1: POST /payments {idempotency_key: "key_abc", amount: 100}
→ Process payment → Return: {id: "pay_123", status: "authorized"}
→ Store: idempotency_store["key_abc"] = response
Request 2: POST /payments {idempotency_key: "key_abc", amount: 100}
→ Found in idempotency_store → Return cached: {id: "pay_123", status: "authorized"}
→ No duplicate charge!
@Service
public class IdempotencyService {
private final RedisTemplate<String, String> redis;
public Optional<PaymentResult> check(String key) {
String cached = redis.opsForValue().get("idempotency:" + key);
if (cached != null) {
return Optional.of(deserialize(cached));
}
// Try to acquire lock
Boolean acquired = redis.opsForValue()
.setIfAbsent("idempotency:" + key + ":lock", "1",
Duration.ofMinutes(5));
if (!acquired) {
throw new PaymentInProgressException();
}
return Optional.empty();
}
public void store(String key, PaymentResult result) {
redis.opsForValue().set("idempotency:" + key,
serialize(result), Duration.ofHours(24));
redis.delete("idempotency:" + key + ":lock");
}
}
4. Payment Methods
4.1 Payment methods
Payment Methods:
├── Card Payments
│ ├── Credit Card (Visa, Mastercard, JCB)
│ ├── Debit Card (ATM cards)
│ └── Prepaid Card
├── Bank Transfer
│ ├── Internet Banking
│ ├── QR Transfer (VietQR)
│ └── Direct Debit
├── E-Wallet
│ ├── MoMo, ZaloPay, ShopeePay
│ └── Apple Pay, Google Pay
├── Buy Now Pay Later
│ ├── Installment plans
│ └── Pay-in-4
└── Alternative
├── Crypto payments
└── Telecom billing
4.2 Checkout Flow Design
┌──────────────────────────────────────────────────────────────┐
│ CHECKOUT FLOW │
│ │
│ Step 1: Step 2: Step 3: Step 4: │
│ Cart Review → Payment Method → Confirm → Result │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌─────────┐ │
│ │ Items │ │ □ Credit Card│ │ Total: │ │ ✓ Paid │ │
│ │ Subtotal │ │ □ Bank │ │ $100 │ │ Receipt │ │
│ │ Shipping │ │ □ MoMo │ │ Pay Now │ │ Order # │ │
│ │ Tax │ │ □ ZaloPay │ │ │ │ │ │
│ └──────────┘ └──────────────┘ └──────────┘ └─────────┘ │
└──────────────────────────────────────────────────────────────┘
5. Retry & Error Handling
5.1 Retry Strategy
Payment Retry Policy:
├── Network timeout → Retry with exponential backoff
├── PSP 5xx error → Retry with different PSP
├── Rate limited → Retry after delay
├── Insufficient funds → No retry (terminal error)
├── Card declined → No retry (terminal error)
└── Fraud blocked → No retry (terminal error)
@Retryable(
value = {PSPTimeoutException.class, PSPServerException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public PSPResponse authorizeWithRetry(Payment payment) {
return pspAdapter.authorize(payment);
}
@Recover
public PSPResponse fallbackPSP(PSPTimeoutException e, Payment payment) {
// Try alternative PSP
var fallbackPSP = router.selectFallback(payment);
return fallbackPSP.authorize(payment);
}
5.2 Webhook Delivery
Payment Result → Webhook Queue → Delivery Engine
│
┌────▼────┐
│ Attempt │
│ 1 │──► merchant.com/webhook
└────┬────┘
│ (Failed)
Wait 1 min
│
┌────▼────┐
│ Attempt │
│ 2 │──► merchant.com/webhook
└────┬────┘
│ (Failed)
Wait 5 min
│
┌────▼────┐
│ Attempt │
│ 3 │──► merchant.com/webhook
└─────────┘
... up to 5 attempts
6. Database Schema
6.1 Core Payment Tables
CREATE TABLE payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
idempotency_key VARCHAR(255) UNIQUE NOT NULL,
merchant_id UUID NOT NULL,
customer_id UUID,
amount BIGINT NOT NULL, -- Amount in smallest currency unit (cents/dong)
currency VARCHAR(3) NOT NULL DEFAULT 'VND',
status VARCHAR(30) NOT NULL DEFAULT 'CREATED',
payment_method VARCHAR(50) NOT NULL,
description TEXT,
metadata JSONB DEFAULT '{}',
psp_reference VARCHAR(255),
auth_code VARCHAR(100),
failure_reason TEXT,
captured_amount BIGINT DEFAULT 0,
refunded_amount BIGINT DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ,
CONSTRAINT valid_status CHECK (status IN (
'CREATED', 'PENDING', 'AUTHORIZED', 'CAPTURED',
'SETTLED', 'VOIDED', 'REFUNDED', 'FAILED', 'EXPIRED'
))
);
CREATE TABLE payment_attempts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
payment_id UUID NOT NULL REFERENCES payments(id),
psp_id VARCHAR(50) NOT NULL,
attempt_no INT NOT NULL,
status VARCHAR(30) NOT NULL,
request_body JSONB,
response_body JSONB,
response_code VARCHAR(10),
error_message TEXT,
latency_ms INT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_payments_merchant ON payments(merchant_id, created_at DESC);
CREATE INDEX idx_payments_status ON payments(status) WHERE status IN ('PENDING', 'AUTHORIZED');
CREATE INDEX idx_payment_attempts_payment ON payment_attempts(payment_id);
Summary
Payment Gateway architecture requires:
- State Machine clear for payment lifecycle
- Idempotency to ensure no duplicate charging
- Two-phase payment (Auth + Capture) for flexibility
- Smart Retry logic with PSP fallback
- Webhook has guaranteed delivery for merchant notification
Next article: Deep-dive into Payment Processing — Authorization, Capture, Settlement workflows and batch processing.