Cross-post. Original: stellarbytecapital.com/blog/cross-border-payment-system-design
A payment system has one job that dwarfs all the others: never lose track of money. Features, UI, and even uptime are negotiable in a pinch — a lost or duplicated transaction is not. Cross-border adds currencies, multiple payment channels, settlement delays, and regulators on top. Get the money-safety core right and everything else is ordinary engineering; get it wrong and no amount of polish saves you.
The ledger is the system
The single most important decision is to make an append-only, double-entry ledger the source of truth — not a mutable balance column you increment. Every movement of money is recorded as balanced entries (a debit and a matching credit) that sum to zero. A user’s balance is derived from the ledger, never stored as the primary fact.
- Immutable entries. You never edit or delete a posting. A mistake is corrected with a new reversing entry, so history is a complete, auditable trail.
- Balances always reconcile. Every entry is balanced, so the whole system sums to zero at all times. If it doesn’t, you have a bug — detectable immediately, not months later in an audit.
- Every entry has a reason. Each posting references the event that caused it, so you can always answer “why is this number what it is?”
A mutable balance is a number you hope is right. A ledger is a number you can prove is right.
Idempotency: the network will retry, so must you survive it
Money movement crosses networks that time out, drop, and duplicate. The classic failure: your service calls a payment channel, the channel processes it, but the response is lost — so a retry charges the user twice. The defense is idempotency, end to end.
- Client-supplied idempotency keys. Every money-moving write carries a unique key. The server records the key with the result; a repeat returns the original outcome instead of doing the work again.
- Exactly-once at the boundary. Calls to external channels are wrapped so a retry never means a second real charge — the same discipline that keeps a trading bot from double-submitting orders.
- Transactional writes. The ledger entry and the state change commit together, in one database transaction. Partial writes are the enemy.
Payment state as an explicit machine
A payment is never simply “done.” Model it as an explicit state machine — initiated → pending → settled, with failed and reversed branches — and persist every transition.
-
Everything is async. Channels confirm out of band, sometimes hours later. Hold a payment in
pendingand resolve it on a callback or poll; never assume synchronous success. - The external channel is the source of truth for its leg. Your local “I think it succeeded” means nothing until the channel confirms. Reconcile against the channel; trust the channel.
Reconciliation: assume drift, detect it daily
No matter how careful the writes, your records and the channels’ records will drift — missed callbacks, timing gaps. Reconciliation is a core scheduled job, not optional cleanup:
- Pull each channel’s settlement report and match it line-by-line against your ledger.
- Flag every mismatch into an exceptions queue a human can work.
- Track a reconciliation watermark so you always know the last point the books were provably correct.
Multi-channel and multi-currency without chaos
- A channel abstraction. Each provider sits behind a common interface (initiate, query, handle-callback, reconcile). Adding a channel is implementing that interface, not rewiring the core.
- Currency as first-class data. Every amount carries its currency, stored in minor units as integers — never floats. FX conversions are themselves ledger events, so the books stay balanced across currencies.
What to avoid
- A mutable balance column as the truth — the original sin; you can’t prove correctness or cleanly reconcile.
- Floats for money — rounding errors compound into unexplainable discrepancies. Integers in minor units, always.
- Assuming synchronous success — how double-charges and phantom balances happen.
- Skipping reconciliation until there’s a problem — by then the drift is large, old, and expensive.
We’re Xingyao Byte — building payment platforms, quant trading systems, and secure AI-execution layers. Remote, async-first → stellarbytecapital.com
답글 남기기