Design a Payment Processing System
1. Problem Statement & Scope Clarification
System Mission
Design a mission-critical, bank-grade global payment processing platform (equivalent to Stripe, Adyen, or Square) engineered to orchestrate millions of card authorizations, captures, and refunds daily. The system must guarantee absolute zero double-charges via distributed idempotency keys, enforce an immutable double-entry bookkeeping ledger, safely navigate third-party Payment Service Provider (PSP) gray failures via stateful saga orchestration, and reconcile billions of dollars nightly against external banking settlement files.
Payment Processing System at a Glance
| Measure | Value |
|---|---|
| Scale | 20M Payments/Day |
| Ingestion Peak | 2,500 TPS |
| Ledger Invariant | ΣDebit = ΣCredit |
| Read QPS | 12,500 Peak |
| P99 SLA | < 800ms (PSP Sync Charge) |
| Durability | 100% Zero Data Loss |
| Idempotency TTL | 7 Days (DynamoDB) |
| Orchestration | AWS Step Functions |
| Ledger DB | Aurora PostgreSQL |
Functional Requirements
- Card Pay-In (Authorization & Capture): Ingest, tokenize, and execute credit card charges via multiple external PSPs (e.g., Stripe, Adyen, Chase Paymentech) with sub-second turnaround. Support two-phase authorize-and-capture (auth-capture) workflows as well as immediate single-phase charges.
- Ironclad Distributed Idempotency: Guarantee that network timeouts, client retries, or duplicate button taps never result in multiple charges or duplicated ledger entries for the same transaction coordinate.
- Immutable Double-Entry Ledger: Record every cent of money movement as balanced credit and debit rows adhering to the fundamental accounting equation (). Account balances must be mathematically derivable from the immutable transaction audit trail.
- Smart Multi-PSP Routing & Failover: Dynamically route payments based on card BIN (Bank Identification Number), issuing country, transaction fees, and live PSP health/authorization rates to maximize payment conversion.
- Asynchronous Settlement & Automated Nightly Reconciliation: Ingest multi-gigabyte daily bank and PSP settlement clearing files (NACHA, CAMT.053, CSV/BAI2), match them against internal ledger entries using automated big-data batch pipelines, and flag discrepancies for human review.
Non-Functional Requirements (SLAs & SLOs)
- Data Durability & Consistency: zero data loss guarantee. Strict ACID transactions for all internal balance and ledger state transitions; no eventual consistency on monetary balances.
- Availability: ("five nines") uptime for the payment ingestion and authorization API (less than 5.26 minutes of unscheduled downtime per calendar year).
- Latency (P99):
- Synchronous Card Charge Path: (including external PSP round-trip latency of ).
- Internal Ledger & Balance Mutations: .
- Balance & Payment Status Inquiries: .
- Security & Regulatory Compliance: PCI-DSS Level 1 compliant architecture. Zero raw Primary Account Numbers (PAN), CVVs, or expiration dates stored in unencrypted databases. Tokenization handled via dedicated isolated HSM-backed vaults using AWS KMS Customer Managed Keys (CMK).
2. Capacity & Scale Estimation (Back-of-the-Envelope Math)
Transaction Volume & Throughput (QPS)
- Daily Completed Transactions: ().
- Average Transaction QPS:
- Peak Ingestion QPS ( diurnal + flash-sale burst):
- Read & Status Inquiry QPS ( Read/Write Ratio):
Storage Footprint & Capacity Growth (5-Year Horizon)
Every financial transaction generates three interconnected records:
- Transaction Header: ID, buyer ID, merchant ID, status, currency, timestamps, metadata .
- Balanced Double-Entry Rows: Minimum 2 entries (Source Debit + Destination Credit) .
- Audit Trail & Settlement State: PSP reference tokens, state transition timestamps .
- Total Storage per Payment: .
- Accounting for B-tree index structures () and 3-AZ storage replication:
Network Bandwidth Sizing
- Average Payment Request/Response Payload: JSON envelope.
- Peak Ingress Bandwidth:
- Peak Egress Bandwidth (Inquiries + PSP Calls):
Ephemeral Idempotency Key Storage (DynamoDB)
- Key Record Size: Idempotency Key (UUID), Payment ID, Status, Cached Response Hash .
- TTL Window: 7 days ().
- Working Dataset in DynamoDB:
3. AWS-First High-Level Architecture
The architecture partitions responsibilities into four distinct domains: Ingress & Perimeter Tokenization, Saga Orchestration & PSP Execution, Core ACID Double-Entry Ledger, and Asynchronous Settlement & Reconciliation.
Synthesizing vector architecture diagram...
Follow a payment from the top. In the "Perimeter, Security & Tokenization" panel, WAF and mutual TLS guard the API, and the tokenizer replaces the card number with a token using HSM-backed keys, so raw card data stays inside this one small PCI-audited area. In the "Ingestion & Fast Idempotency Tier" panel, the payment API checks the idempotency key in DynamoDB, so a retried request returns the original result instead of charging twice, and queues the payment in SQS FIFO. In the "Distributed Payment Saga & PSP Execution" panel, Step Functions runs the payment and its smart router picks Stripe, falls back to Adyen, and then to Chase if a provider fails. The "Immutable Double-Entry Ledger Tier" records every movement as balanced debit and credit rows in Aurora, and the "Asynchronous Settlement & Reconciliation Data Lake" streams the ledger to S3 and matches it daily against bank files, paging on any mismatch.
Data Flow Walkthrough
- Tokenization: The client streams raw card data directly to the isolated
TokenizerSvc. Card numbers are encrypted using an envelope encryption key from AWS KMS/CloudHSM; only a synthetic token (tok_visa_9918) is returned to the client and forwarded to the payment backend. - Ingress & Idempotency Locking: The client submits
POST /v1/paymentswith anIdempotency-Keyheader. The ECS Payment API initiates a conditional write to DynamoDB (attribute_not_exists(idempotency_key)). If the key already exists and isSUCCEEDED, the cached response is returned immediately. IfIN_FLIGHT, HTTP 409 Conflict is returned. - Saga Orchestration: A fresh transaction begins by launching an AWS Step Functions Express/Standard Workflow. The state machine creates an internal record in Aurora PostgreSQL marked
PENDING. - Smart Routing & Charge Execution: The state machine evaluates routing weights (BIN, latency, fees) and invokes the chosen external PSP API over mTLS.
- Ledger Commit: Upon receiving an authorization/capture acknowledgement from the PSP, the saga opens an atomic transaction in Aurora PostgreSQL, inserts balanced debit and credit entries into
ledger_entries, updates the transaction state toCOMMITTED, and updates DynamoDB toSUCCEEDED. - Nightly Settlement Reconciliation: Daily clearing reports from banks and PSPs are ingested via AWS Transfer Family into S3. An AWS Glue Spark job performs a full outer join against internal ledger entries, ensuring zero financial drift.
Core Request Tracing Execution Walkthrough
| Step # | Event / Action | Component State | Distributed Transition | Output / Response |
|---|---|---|---|---|
| Step 1 | Client submits raw PAN payload to isolated tokenization service | Ephemeral memory buffer in Lambda (zero persistence) | Envelope encryption via KMS CMK (v2); synthetic token minted | tok_visa_4242_pci_token returned to client SDK |
| Step 2 | Client dispatches POST /v1/payments with Idempotency-Key | API Gateway / ECS Ingestion Fleet | Conditional PutItem(attribute_not_exists(PK)) in DynamoDB | Lock acquired with lease status="IN_FLIGHT" (or cached 200) |
| Step 3 | Step Functions launches payment saga state machine | Saga Orchestration Coordinator | INSERT INTO payment_transactions with status='PENDING' in Aurora | Transaction record staged with unique ID pay_live_718293847501 |
| Step 4 | Smart router evaluates BIN, latency, and fees; calls PSP API | Smart Router Fleet (mTLS HTTPS) | Outbound POST /v1/charges dispatched to primary PSP (Stripe) | PSP returns synchronous 200 OK (ch_stripe_3N8vK2Lkd0918) |
| Step 5 | Socket timeout / network drop occurs during outbound call | Saga enters INDETERMINATE recovery state | Status Inversion query GET /v1/charges with exponential full jitter | PSP confirms capture status or triggers safe transaction abort |
| Step 6 | Open atomic transaction in Aurora PostgreSQL Primary | Relational Ledger Engine (READ COMMITTED) | Insert balanced lines: Debit PSP Receivable, Credit Merchant, Credit Revenue | Status COMMITTED () |
| Step 7 | Finalize idempotency record in DynamoDB table | DynamoDB Idempotency Table | UpdateItem sets status="SUCCEEDED" with cached JSON payload | HTTP 201 Created returned to client with full payment receipt |
4. API Interface Design & Data Contracts
1. Execute Charge (POST /v1/payments)
Executes an idempotent payment charge against a tokenized payment instrument.
Request Headers
httpPOST /v1/payments HTTP/1.1 Host: api.payments.platform.aws.internal Authorization: Bearer sec_live_99a8b7c6d5e4f3a2b1 Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d X-Correlation-ID: corr_01J8N6K3P4V9QZ2W8M1Y7R4X Content-Type: application/json
Request Payload
json{ "amount_cents": 4999, "currency": "USD", "payment_method_token": "tok_visa_4242_pci_token", "customer_id": "cust_usr_88124", "order_id": "ord_ecommerce_990145", "merchant_account_id": "m_acct_nike_store_us", "capture_mode": "AUTOMATIC", "metadata": { "shipping_country": "US", "ip_address": "198.51.100.42" } }
Response: 201 Created
json{ "payment_id": "pay_live_718293847501", "status": "SUCCEEDED", "amount_cents": 4999, "currency": "USD", "psp_reference": "ch_stripe_3N8vK2Lkd0918", "routing_selected_psp": "STRIPE", "idempotency_key": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "captured": true, "fee_cents": 175, "net_merchant_cents": 4824, "created_at": "2026-09-16T14:32:01.458Z" }
2. Capture Authorized Hold (POST /v1/payments/{payment_id}/capture)
Captures previously authorized funds (e.g., after physical order shipment).
httpPOST /v1/payments/pay_live_718293847501/capture HTTP/1.1 Host: api.payments.platform.aws.internal Idempotency-Key: idemp_cap_882194 Content-Type: application/json { "amount_cents": 4999, "currency": "USD" }
3. Issue Full or Partial Refund (POST /v1/payments/{payment_id}/refunds)
httpPOST /v1/payments/pay_live_718293847501/refunds HTTP/1.1 Host: api.payments.platform.aws.internal Idempotency-Key: idemp_ref_330198 Content-Type: application/json { "amount_cents": 2000, "reason": "CUSTOMER_RETURN" }
Status Codes & Error Contracts
200 OK: Idempotent query or repeat of previously successful idempotent request.201 Created: Payment newly executed and ledger successfully committed.400 Bad Request: Validation failure (malformed payload, invalid currency).402 Payment Required: PSP declined (insufficient funds, expired card, fraud rule rejection).409 Conflict: Idempotency key currentlyIN_FLIGHTon another worker; client must retry after 500ms.422 Unprocessable Entity: Currency mismatch or amount exceeds authorized ceiling.429 Too Many Requests: Client or tenant rate limit exceeded.504 Gateway Timeout: PSP call timed out; transaction placed in indeterminate gray-failure recovery.
json{ "error": { "code": "CARD_DECLINED", "message": "Your card was declined due to insufficient funds.", "decline_code": "insufficient_funds", "payment_id": "pay_live_718293847501", "psp_code": "do_not_honor" } }
5. Data Models & Storage Architecture
Database Selection Justification
- Amazon Aurora PostgreSQL Multi-AZ: Selected for the core ledger. Money movement requires serializable row integrity, multi-record ACID transactions, check constraints, and relational queries for double-entry balancing.
- Amazon DynamoDB: Selected for the low-latency distributed idempotency key cache. Supports sub-5ms conditional writes (
PutItemwithattribute_not_exists) and automatic 7-day TTL cleanup with zero operational burden. - Amazon S3 + AWS Glue + Amazon Athena: Selected for the financial data lake and nightly reconciliation engine. Handles petabyte-scale CSV, Parquet, and bank clearing statements at minimum storage cost.
1. Double-Entry Bookkeeping Ledger DDL (Aurora PostgreSQL)
In financial systems, balances are never mutated via raw UPDATE accounts SET balance = balance + 10 statements. Balances are mathematically derived from an immutable audit trail of balanced debit and credit entries. To eliminate fractional-cent rounding drift across international currencies and fee splits, monetary amounts are modeled as 64-bit Signed Integers in Micro-Units ( base currency units, where ).
sql-- 1. Supported Currency Metadata Table CREATE TABLE currencies ( currency_code CHAR(3) PRIMARY KEY, -- 'USD', 'EUR', 'JPY', 'GBP' exponent INT NOT NULL DEFAULT 2, -- Minor unit exponent (USD=2, JPY=0, BHD=3) micro_multiplier BIGINT NOT NULL DEFAULT 1000000 -- Conversion to micro-units ); -- 2. Accounts Master Table CREATE TABLE accounts ( account_id VARCHAR(64) PRIMARY KEY, -- e.g. "USER_WALLET:usr_102", "MERCHANT_PAYABLE:m_402", "PSP_RECEIVABLE:stripe" account_type VARCHAR(32) NOT NULL, -- ASSET, LIABILITY, EQUITY, REVENUE, EXPENSE currency CHAR(3) NOT NULL REFERENCES currencies(currency_code), status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', -- Read-optimized SNAPSHOT of SUM(ledger credits) - SUM(ledger debits). It is never written by -- application code directly: it is updated in the same transaction that inserts the ledger rows -- (Section 9.2) and is re-derived from ledger_entries by the nightly audit job. balance_micros BIGINT NOT NULL DEFAULT 0 CHECK (balance_micros >= 0), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 3. Master Payment Transactions Table CREATE TABLE payment_transactions ( transaction_id VARCHAR(64) PRIMARY KEY, -- e.g. "pay_live_718293847501" idempotency_key VARCHAR(128) UNIQUE NOT NULL, merchant_account_id VARCHAR(64) NOT NULL REFERENCES accounts(account_id), customer_id VARCHAR(64) NOT NULL, amount_micros BIGINT NOT NULL CHECK (amount_micros > 0), fee_micros BIGINT NOT NULL DEFAULT 0 CHECK (fee_micros >= 0), currency CHAR(3) NOT NULL REFERENCES currencies(currency_code), status VARCHAR(20) NOT NULL CHECK (status IN ('PENDING', 'COMMITTED', 'REJECTED', 'REFUNDED', 'INDETERMINATE')), psp_provider VARCHAR(32) NOT NULL, -- STRIPE, ADYEN, CHASE psp_reference VARCHAR(128), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 4. Immutable Double-Entry Ledger Lines CREATE TABLE ledger_entries ( entry_id BIGSERIAL PRIMARY KEY, transaction_id VARCHAR(64) NOT NULL REFERENCES payment_transactions(transaction_id), account_id VARCHAR(64) NOT NULL REFERENCES accounts(account_id), direction VARCHAR(6) NOT NULL CHECK (direction IN ('DEBIT', 'CREDIT')), amount_micros BIGINT NOT NULL CHECK (amount_micros > 0), currency CHAR(3) NOT NULL REFERENCES currencies(currency_code), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Indexing for Fast Settlement & Auditing Queries CREATE INDEX idx_ledger_account_lookup ON ledger_entries(account_id, created_at DESC); CREATE INDEX idx_ledger_tx_id ON ledger_entries(transaction_id); CREATE INDEX idx_payment_created ON payment_transactions(created_at DESC);
The Fundamental Accounting Invariant & Fractional-Cent Drift Protocol
Every transaction must mathematically balance across micro-units:
Fractional-Cent Rounding Drift in Multi-Currency & Fee Allocations
When converting currencies or splitting percentage fees across basket items (e.g., splitting a $10.00 charge across 3 merchant payouts yielding $3.333333... each), integer division causes fractional cents to drift. If unhandled, , violating the double-entry invariant.
To eliminate drift:
- Banker's Rounding (Half-Even): Standardized to IEEE 754-2008 half-even rounding to prevent statistical accumulation toward zero or infinity.
- Micro-Unit Precision: Calculations execute in micro-units prior to display truncation.
- Synthetic Rounding Variance Line: If an irreducible fractional remainder exists (), the transaction engine generates a compensatory balancing ledger line attributed to a dedicated system equity account (
EQUITY:FX_ROUNDING_VARIANCE): The engine appends(account: "EQUITY:FX_ROUNDING_VARIANCE", direction: (Δ > 0 ? "CREDIT" : "DEBIT"), amount: |Δ|). This preserves strict mathematical equality down to the single micro-unit without manual journal interventions.
Zero-Sum Ledger Invariant & Micro-Unit Precision
Financial platforms must never use floating-point types (FLOAT, DOUBLE) or naive 2-decimal currencies. Representing all monetary values as 64-bit signed integers in fixed micro-units ( base currency) alongside IEEE 754-2008 Banker's Rounding guarantees that rounding bias does not accumulate. Any residual sub-cent drift () is transparently balanced via the synthetic EQUITY:FX_ROUNDING_VARIANCE journal entry, keeping .
| Account ID | Account Type | Direction | Amount (USD) | Micro-Units |
|---|---|---|---|---|
PSP_RECEIVABLE:stripe | Asset | DEBIT | ||
MERCHANT_PAYABLE:nike | Liability | CREDIT | ||
PLATFORM_FEE_REVENUE | Revenue | CREDIT | ||
EQUITY:FX_ROUNDING_VARIANCE | Equity | CREDIT | ||
| Total | Debit: $49.99 | Credit: $49.99 | Net: 0 micro-units |
2. DynamoDB Single-Table Idempotency Schema (PaymentIdempotencyTable)
Partition Key (PK) | Sort Key (SK) | Attributes | Description |
|---|---|---|---|
IDEMP#<idempotency_key> | STATE | status="IN_FLIGHT", created_at=1726497121, lease_expires_at=1726497181 (60 s), ttl=1727101921 (7 d) | Initial atomic lock acquisition. lease_expires_at bounds how long a concurrent caller gets HTTP 409 before falling back to Aurora (Section 8.4); ttl is DynamoDB's row expiry |
IDEMP#<idempotency_key> | STATE | status="SUCCEEDED", payment_id="pay_7182", response_payload="{...}" | Final committed cached state |
IDEMP#<idempotency_key> | STATE | status="FAILED", error_code="CARD_DECLINED" | Explicit terminal decline state |
3. PCI-DSS Tokenization Vault Schema & KMS Envelope Key Rotation
To comply with PCI-DSS Level 1 while permitting seamless annual and emergency key rotation, PAN data is encrypted using envelope encryption. Raw cards are encrypted under unique Data Encryption Keys (DEKs) wrapped by an AWS KMS Customer Managed Key (CMK).
sqlCREATE TABLE card_token_vault ( token_id VARCHAR(64) PRIMARY KEY, -- e.g. "tok_live_991823a" customer_id VARCHAR(64) NOT NULL, kms_key_arn VARCHAR(256) NOT NULL, -- ARN of root CMK used key_version VARCHAR(16) NOT NULL, -- e.g. "v1", "v2" encrypted_dek BYTEA NOT NULL, -- AES-256 DEK wrapped by KMS CMK ciphertext_pan BYTEA NOT NULL, -- Primary Account Number encrypted with DEK pan_last4 CHAR(4) NOT NULL, -- Plaintext masked last 4 digits for UI display card_brand VARCHAR(20) NOT NULL, -- VISA, MASTERCARD, AMEX expiration_month INT NOT NULL, expiration_year INT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), reencrypted_at TIMESTAMPTZ -- Populated by background key rotation daemon ); CREATE INDEX idx_vault_key_version ON card_token_vault(key_version) WHERE reencrypted_at IS NULL;
Tokenization Key Rotation Workflow
- Active Write Key Rotation: AWS KMS CMK rotates automatically or administrators create a new key version (
v2). New tokens immediately usev2. - Dual-Key Transparent Decryption: Decryption inspects
kms_key_arnandkey_version. KMS decrypts the token'sencrypted_dekusing the corresponding key version, enabling zero-downtime transitions. - Asynchronous Batch Re-Encryption: An AWS ECS background daemon runs off-peak, querying rows where
key_version != current_version. It decrypts using the legacy key, re-wraps with the new CMK underv2, updatesencrypted_dek, and commits without modifyingtoken_idor disrupting client operations.
Zero-Downtime Envelope Key Rotation
By coupling envelope encryption (DEK encrypted by root KMS CMK) with explicit key_version column metadata, key rotation becomes completely non-blocking. Live payment traffic transparently decrypts using the key version embedded in the token record, while an asynchronous background daemon re-encrypts stored records to v2 off-peak without table locking.
Unlock Complete Architecture & Production Runbooks
You have explored the free architectural preview (~40%). Spend 1 Coin to unlock the remaining 6 production deep-dive sections for a full 24 hours.