Skip to content

How a Stripe-like payments platform might evolve

Developers integrate once and expect money to move correctly forever. How API design, idempotency keys, double-entry ledgers, strong consistency and reliable webhooks shape a payments platform.

The user problem and the hard parts

The users are developers. They want to accept a payment with a few lines of code, trust that a retry never charges a customer twice, and learn reliably about everything that happens later (refunds, disputes, payouts). Behind that simple API sits a web of card networks, banks and regulators.

The hard engineering problems:

  • API design: once thousands of businesses depend on a field name, you can never change it. The API is a long-term contract.
  • Idempotency: networks fail after the server did the work. The client cannot know whether the charge happened, so it retries. Retries must be safe.
  • Ledgers and double-entry: every movement of money must be recorded so that balances can be proven, audited and reconciled with banks.
  • Strong consistency: an eventually consistent balance means paying out money you don't have. Money paths need guarantees.
  • Webhooks: telling merchants about asynchronous events reliably, in a world where merchants' servers are often down.
  • Reliability: an outage is not lost page views; it is lost sales for every merchant at once.

How the architecture might evolve

A Stripe-like payments platform

A plausible architecture for a payments API platform at each scale. Stripe has publicly documented its idempotency keys, API versioning and webhooks; internal systems described here are illustrative.

  1. 1
    Stage 1 · First merchants
    100 merchants · 1k charges/day
    Versioned REST API monolithRelational database (single primary)One acquiring bank / processor integrationIdempotency key tableWebhook sender with retries

    A single primary database gives strong consistency for every money movement. Idempotency and webhooks are designed in from day one because they are part of the public contract and cannot be retrofitted cleanly.

  2. 2
    Stage 2 · Growth
    10k merchants · 1M charges/day
    Double-entry ledger as its own moduleQueue + workers for webhooks, payouts, reportsMultiple processor integrations with routingRead replicas for dashboardsFraud scoring on every charge

    The ledger becomes the source of truth for balances, separate from the API objects. Webhook delivery leaves the request path so a slow merchant endpoint can't slow down charges.

  3. 3
    Stage 3 · Platform
    Millions of merchants · thousands of requests/s
    Sharded ledger and payment stores (by merchant)Event log of every state change (outbox → stream)API version transformation layerMulti-zone replication with synchronous commitsReconciliation pipelines against bank files

    No single database holds all merchants, so data is sharded by account; a payment rarely spans shards. Every state change is an event that drives webhooks, ledgers and analytics.

  4. 4
    Stage 4 · Global financial infrastructure
    Global · many products (billing, payouts, issuing)
    Multi-region deployment with regional data residencyShared ledger platform across productsCell-based isolation to limit blast radiusExtensive automated reconciliation and audit

    Regulation requires data to stay in some regions. Splitting infrastructure into isolated cells means a bad deploy or overloaded shard affects a fraction of merchants, not all of them.

One charge, exactly once

A plausible architecture for Stage 3 might look like this. Follow a charge request, including what happens when the client retries.

Click a component

Click any component to see what it does.

Deep dive: idempotency keys and ledgers

bash
curl https://api.example-payments.com/v1/charges \
  -u sk_test_123: \
  -H "Idempotency-Key: order-8812-attempt-1" \
  -d amount=2000 -d currency=eur -d source=tok_visa
Stripe publicly documents this pattern: the client chooses the key and reuses it on retries.

Idempotency keys turn an unsafe operation (POST) into a safe-to-retry one. Rules that make them work: keys are scoped per account, stored with a hash of the request body, kept for a bounded period (e.g. 24 hours), and the stored response is replayed, including errors.

Double-entry ledger. Each transaction is a set of entries across accounts whose debits equal their credits (see the table below). Balances are derived by summing entries. You never "set" a balance, so every number can be explained by history.

AccountDebitCredit
Customer card (external)20.00
Merchant pending balance19.42
Platform fee revenue0.58
One €20 charge as balanced ledger entries.

Engineering trade-offs

Mutable balance column

  • Simple to build
  • Fast reads
  • No history of why a balance is what it is
  • Lost updates under concurrency
  • Impossible to audit
Choose when: Never for real money. Fine for loyalty points in a prototype.

Append-only double-entry ledger

  • Every balance is explainable
  • Errors are corrected by new entries, not edits
  • Reconciles with banks
  • More storage and modelling effort
  • Balances need materialised views for speed
Choose when: Any system that moves or owes real money.

Availability-first (eventually consistent) money store

  • Stays writable during partitions
  • Two regions can both approve spending the same balance
Choose when: Analytics on payments, never the authoritative balance.

Consistency-first store per shard

  • No double spending
  • Simple correctness reasoning
  • A shard may reject writes during failover
  • Cross-shard operations need care
Choose when: Authoritative balances and payment state. See the .

Practical exercise

The double-charged customer

A merchant's checkout calls your API to charge €80. Your server authorises the card, commits, and the response is lost when the merchant's load balancer times out at 10 s. The merchant code retries without an idempotency key and the customer is charged twice. What should the API, the merchant and your platform each do?