Payments, an interactive architecture walkthrough
Stripe-style payments platform: how the architecture works
How a payments platform tokenises cards, authorises a payment through the card networks, records it in a ledger, and settles funds to merchants.
This is a learning explainer based on public engineering writing and general design practice. It is simplified, it is not the actual architecture of any company, and it may be inaccurate.
Interactive diagram
Overview
A payments platform keeps raw card data out of merchants' hands by tokenising it in an isolated, PCI-scoped vault. A payment is a state machine driven by an orchestration service: it is screened for fraud, routed to an acquirer, authorised by the card network and issuing bank, and recorded in a double-entry ledger. Idempotency keys are stored durably with the payment, and events leave through an outbox. Settlement, reconciliation and webhooks run asynchronously. This is a generic reference design, not Stripe's internal architecture.
Trade-offs and what changes at scale
Correctness beats speed here: idempotency keys and PaymentIntent state are written in one database transaction, so a retry with the same key returns the original result instead of repeating the work (keys are kept for a limited window, and a retry with changed parameters is rejected). The acquirer call cannot be part of that transaction, so the outcome of an in-flight authorisation is settled by idempotent acquirer requests, timeouts and reconciliation, and outbox events are at-least-once. Webhooks are at-least-once, so merchants must deduplicate on the event id. Keeping the vault in its own network zone narrows PCI scope but adds a hop and a service that must be extremely available. At scale, the ledger and payments database are sharded by merchant or account with hot merchants isolated, card authorisation is synchronous but capture, payouts and reconciliation are batched and asynchronous, and routing across several acquirers improves approval rates and resilience.
How it works, step by step
The walkthrough above, as text. Each step lights up the components named under it.
The shopper types card details into SDK-hosted fields, so the merchant's page never handles raw card data. This typically keeps the merchant in the lightest PCI self-assessment (SAQ A); the merchant still owns the security of the page that hosts the fields.
The SDK sends the card through a hardened public tokenisation endpoint (WAF, rate limiting, publishable-key check) to the vault, which encrypts it with keys from the HSM-backed KMS and returns a single-use token.
The merchant server creates the payment with its secret API key and an idempotency key. The gateway authenticates, rate-limits (counters live in a cache) and passes the idempotency key on, so a retry with the same key returns the original result instead of repeating the work.
Orchestration writes the PaymentIntent and the idempotency key in the same Postgres transaction, so a retry with the same key returns the original result instead of repeating the work, and drives the state machine. Every transition is stored, so a crash can resume safely.
Risk scoring evaluates the payment before money moves and returns allow, review or block, or asks for 3-D Secure (strong customer authentication): the intent moves to requires_action while the cardholder confirms with their bank (the challenge runs between the cardholder and the issuer's authentication server), which many cards in the EU and India require and which often shifts fraud liability to the issuer. Blocking early is cheaper than a fraud chargeback.
The routing service picks the acquirer using cost, success rate and availability, and can fail over when one degrades.
The call goes out through the vault's proxy, which swaps the token for the real card number inside the PCI zone, so the card number never reaches the orchestration, routing or any other service outside the PCI zone. The vault does send it onward to the acquirer.
The authorisation goes through the vault proxy to the acquirer, then the card network, then the issuing bank, which approves or declines. The response flows back and the intent moves to its next state.
The ledger writes balanced double-entry rows in one ACID transaction: entries always balance (debits equal credits) and it is the source of truth for balances. In this design authorisation posts hold entries and capture posts the money entries.
Events are written to an outbox in the same transaction as the state change and relayed to Kafka, so a crash cannot silently drop one (relaying is at-least-once). Webhook delivery signs each payload and retries with backoff for a bounded period, then marks the delivery failed (the merchant can replay it); delivery is at-least-once, so merchants deduplicate by event id.
By default capture happens automatically when the payment is confirmed. With manual capture the intent waits in requires_capture and the merchant calls the API again, either right away or after shipping; that capture also goes to the acquirer through the vault proxy. Orchestration then moves the intent to succeeded and posts the money ledger entries.
Asynchronously, funds arrive from the acquirer in the platform's bank account, and settlement pays merchants by bank transfer from their available balance, recording payout entries in the ledger.
Reconciliation ingests acquirer settlement files and compares them with the ledger. Mismatches are flagged before payouts are trusted.
Refunds enter as API calls from the merchant server or dashboard (they are separate objects from the PaymentIntent), and disputes (chargebacks) arrive through the acquirer's and card network's dispute feeds onto the event bus, which orchestration consumes. Both run through the orchestrator and ledger and notify the merchant, and the dashboard shows their status.
Every service emits metrics and traces, and sensitive services write to the append-only audit log. This supports incident response and PCI evidence.
Sequence diagram
The same walkthrough as a UML sequence diagram, one lane per layer or group (28 components in all), left to right in the order a request reaches them, numbered by step. A loop on a lane is a step that stays inside that group. Solid arrows are synchronous calls, dashed are asynchronous.
Key components
Merchant Sideboundary
- Cardholder: Pays on merchant site
- Client-side SDK: Tokenises card in browser
- Merchant Server: Creates payments, receives webhooks
- Merchant Website/App: Checkout page with client SDK
PCI Zone (Isolated Network)boundary
- Card Vault / Tokenisation: Stores PANs, issues tokens
- Encrypted Card Store: Encrypted card data (postgresql)
- HSM-backed KMS: Key management, encryption keys
Public API Edgelayer
- API Gateway: Auth, rate limits, idempotency keys
- Dashboard: Merchant payments & payouts UI
- Merchant & Key Store: merchants, API keys, settings
- Rate Limit Store: Rate counters (cache)
- Tokenisation Gateway: WAF, rate limits, publishable-key check
Payments Coredomain
- Payments DB: PaymentIntents, idempotency keys, outbox (postgresql)
- Payments Orchestration: Owns PaymentIntent state machine
- Risk & Fraud Scoring: Allow, review, block or require 3-D Secure
- Routing Service: Chooses acquirer/processor
External Financial Networkboundary
- Acquirer / Processor: Merchant's acquiring bank
- Bank Transfer Rails: Merchant payouts
- Card Networks: Visa / Mastercard
- Issuing Bank: Approves or declines
Money Movementdomain
- Acquirer Files: Settlement files
- Double-Entry Ledger: Immutable balanced entries
- Ledger DB: ACID ledger store (postgresql)
- Reconciliation: Matches acquirer files to ledger
- Settlement & Payout: Batches funds, pays merchants
Eventing & Notificationslayer
- Event Bus: payment events (kafka)
- Webhook Delivery: Signed, retried notifications
Observability & Auditlayer
- Audit Log: Append-only audit trail
- Metrics & Tracing: Monitoring, alerting
Sources
Public writing this explainer draws on. Read these for what the real systems actually do.
- Stripe docs: Idempotent requests
- Stripe docs: How PaymentIntents and SetupIntents work (lifecycle)
- Stripe docs: Receive Stripe events in your webhook endpoint
- Stripe blog: Designing robust and predictable APIs with idempotency
- Stripe blog: Ledger, Stripe's system for tracking and validating money movement
Make it yours
Copy this diagram and its walkthrough into your own workspace, then change anything: swap Postgres for DynamoDB, add a cache, rewrite the narration. Free, and it uses no AI credits.
Related walkthroughs
- Digital walletHow a digital wallet records every movement in a double-entry ledger, makes retries safe with idempotency keys, and reserves funds for withdrawals.
- Stock trading platformHow a trading platform sequences every order into a deterministic in-memory matching engine, replicates it by replay, and settles trades separately.
- Notification system at scaleHow a notification platform validates requests, applies user preferences, queues by priority, and delivers push, SMS and email with retries and fallbacks.
This is a learning explainer based on public engineering writing and general design practice. It is simplified, it is not the actual architecture of any company, and it may be inaccurate. Real systems differ in the details. Browse the library.