Payments, an interactive architecture walkthrough
Digital wallet: how the architecture works
How a digital wallet records every movement in a double-entry ledger, makes retries safe with idempotency keys, and reserves funds for withdrawals.
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 digital wallet holds a balance for each user and lets them top up from a card or bank, send money to other users, pay merchants and withdraw. The core is a double-entry ledger: every movement is a transfer that debits one account and credits another for the same amount, including the platform's own accounts for money held at banks and for fees. Balances are computed from entries, never overwritten.
Each transfer is posted in one database transaction that also enforces that a balance cannot go negative, and an idempotency key makes retries safe. Withdrawals use a two-step pending transfer that reserves funds until the bank confirms. Events are written to an outbox in the same transaction and published afterwards, and a daily reconciliation compares the ledger with bank and card reports. This is a simplified learning design, not any company's production system.
Trade-offs and what changes at scale
Strict transactions keep money correct but make hot accounts, such as a large merchant or a fee account, a point of contention, so those are often split into sub-accounts or batched. Pending transfers protect against double spending during slow external payments but leave reserved funds unavailable. Outbox publishing adds a little delay before notifications, in exchange for never announcing a transfer that did not happen. At larger scale the ledger is sharded by account, and transfers across shards need a two-phase approach or a dedicated ledger database.
How it works, step by step
The walkthrough above, as text. Each step lights up the components named under it.
The Wallet App signs in through the Auth Service, which binds the session to a device key and asks for step-up authentication before sensitive actions. A stolen password alone should not be enough to move money.
To add money, the Top-up Service authorises the user's card with the Card Processor. Only after the authorisation succeeds does it ask the ledger to credit the wallet.
The Ledger Service records the top-up as a double-entry transfer: debit the platform's card-funds account and credit the user's wallet account. Balances are derived from these entries, never edited directly.
A send to a friend goes to the Transfer Service, which checks the sender's KYC tier limits and asks the Risk Engine to score the transfer. Risky transfers are held for review instead of being posted.
Every request carries an idempotency key, and the Idempotency Store returns the original result if the key was seen before. A retried request after a timeout then cannot send the money twice.
In one transaction on the Ledger DB, the ledger debits the sender, credits the receiver and checks the sender's balance cannot go below zero. If the check fails the whole transfer is rejected, so no partial transfer exists.
The same transaction writes an event row to the Outbox. Publishing from the outbox afterwards guarantees an event exists for every committed transfer and for none that rolled back.
A relay moves outbox rows onto Ledger Events, which feed receipts through the Notification Service and history in Statements & Analytics. These readers can lag without affecting balances.
A withdrawal first creates a pending debit that reserves the funds, then the Payout Service sends the bank transfer. When the bank confirms, the pending transfer is posted; if it fails, it is voided and the money returns.
At Merchant Checkout, a payment is the same kind of ledger transfer, from the user's wallet to the merchant's account plus a fee account. Reusing one transfer primitive keeps every money movement auditable the same way.
Every day, Reconciliation matches ledger entries against bank and card settlement files and flags anything that does not match. External systems are the final word on real money, so differences are investigated, not ignored.
Sequence diagram
The same walkthrough as a UML sequence diagram, one lane per layer or group (19 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
Clientslayer
- Merchant Checkout: pay with wallet at a shop
- Wallet App: balance, top up, send, pay
Identity & Riskdomain
- Auth Service: login, step-up auth, device keys
- KYC Service: identity checks, account limits
- Risk Engine: scores each transfer for fraud
Paymentsdomain
- Bank Rails: bank transfers in and out
- Card Processor: card authorisation and capture
- Payout Service: withdraw to bank
- Top-up Service: card or bank funding
- Transfer Service: P2P sends and merchant payments
Ledgerboundary
the source of truth for money
- Idempotency Store: request key -> result (postgresql)
- Ledger DB: accounts, transfers, entries (postgresql)
- Ledger Service: double-entry postings, balance checks
- Outbox: events written with each transfer (postgresql)
Asyncdomain
- Ledger Events: posted transfers (kafka)
- Notification Service: receipts and alerts
- Reconciliation: matches ledger to bank and card reports
- Statements & Analytics: history, reporting (clickhouse)
API Gateway
- API Gateway: auth, device binding, rate limits
Sources
Public writing this explainer draws on. Read these for what the real systems actually do.
- TigerBeetle docs: Data modeling (debits and credits, ledgers, idempotent ids, balance limits)
- TigerBeetle docs: Two-phase transfers (pending, post, void, timeout)
- formancehq/ledger: programmable core ledger with idempotency keys
- PostgreSQL docs: Concurrency control (isolation levels, serialization failures)
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
- Stripe-style payments platformHow a payments platform tokenises cards, authorises a payment through the card networks, records it in a ledger, and settles funds to merchants.
- Stock trading platformHow a trading platform sequences every order into a deterministic in-memory matching engine, replicates it by replay, and settles trades separately.
- Ride-hailing platformHow a ride-hailing platform streams driver locations, matches a rider to a nearby driver, prices the trip with surge, and takes payment.
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.