Markdown Editor

Digital Wallet System Design Template

Stored-value wallet: double-entry ledger, cross-shard transfer saga vs 2PC vs event sourcing, idempotency, hot accounts, reconciliation, limits and KYC hooks

Conception systèmeCompletdesigndigitalwallet

Mode d'emploi: Use it when designing balances and transfers for a wallet or stored-value product. Fill the money invariants (2.2) and the cross-shard transfer deep dive (7.2) first, then reconciliation levels (7.5). The main pitfall is treating the balance column as the truth instead of a projection of an append-only ledger.

Aperçu

System Design: [Digital wallet name]

Reference design for a stored-value digital wallet: customer balances, peer-to-peer and merchant transfers, top-ups and withdrawals. It covers a double-entry ledger as the source of truth, cross-shard transfers with a saga versus two-phase commit versus event sourcing, idempotency keys, hot-account contention, reconciliation with banks and payment providers, limits and KYC (know your customer) hooks, and the operational controls a regulated wallet needs. It follows the structure of the System Design Document template and is pre-filled with a worked design to adapt. Replace every [bracketed] value and every row marked Example; values marked Example are assumptions, not benchmarks. Regulatory notes describe a documentation format only. This is a documentation format, not legal advice.

Document control

FieldValue
Document ID[SDD-WAL-001]
System / feature[Stored-value wallet and transfers]
Version[0.1]
Status[Draft / In review / Approved / Implemented / Superseded]
Owner[@owner]
Author(s)[@author]
Approvers / reviewers[@reviewer-1], [@reviewer-2]
Last updatedYYYY-MM-DD
Classification[Confidential]
Licence or regulatory basis[E-money / money transmission / partner-bank programme; jurisdiction]
Related[PRD], [Payment ledger design], [Fraud design], [Threat model], [DPIA]

Approvals

RoleNameDecisionDate
Tech lead[@name][Approved / Changes requested]YYYY-MM-DD
Staff / principal engineer[@name][Decision]YYYY-MM-DD
Finance and treasury[@name][Decision]YYYY-MM-DD
Compliance (AML and KYC)[@name][Decision]YYYY-MM-DD
Security[@name][Decision]YYYY-MM-DD
SRE / operations[@name][Decision]YYYY-MM-DD

Revision history

VersionDateAuthorChange summary
0.1YYYY-MM-DD[@name]First draft for design review
0.2YYYY-MM-DD[@name]Example: Replaced two-phase commit with a saga through an in-flight account
0.3YYYY-MM-DD[@name]Example: Added sub-balances for merchant hot accounts
1.0YYYY-MM-DD[@name]Example: Approved

Contents

1. Overview

1.1 Problem statement

[Example: Our app lets 50 million customers hold a balance, pay each other and pay 300 000 merchants. Balances live in a single relational table that is updated in place. Last quarter a retry storm after a database failover applied 1 900 transfers twice, and finance needed 11 days to find them because there was no ledger to replay. The largest merchant receives so many credits during lunch hours that its balance row became a lock queue and transfer latency reached 8 seconds. Compliance cannot show which limits applied to a given transfer at the time it ran.]

1.2 Proposed solution

[Example: An append-only double-entry ledger becomes the source of truth. A balance is a derived, cached projection of ledger entries per account, updated in the same transaction as the entries. Accounts are sharded by account ID. A transfer between two shards is a saga: debit the sender into an in-flight (suspense) account on the sender shard, then credit the receiver from the in-flight account on the receiver shard, with an outbox carrying the second step. Every request carries an idempotency key that is stored with the result. Merchant hot accounts are split into sub-balances. A limits and risk service runs before the debit and records the rule version it applied. Reconciliation runs continuously against internal invariants and daily against bank and provider statements.]

1.3 Decisions requested from reviewers

IDQuestionRecommended optionSection
Q-01Example: Cross-shard transfer mechanism?Saga through an in-flight account, not two-phase commit7.2, 10
Q-02Example: Is the balance stored or computed?Stored projection updated atomically with entries; recomputable from entries7.1
Q-03Example: Hot merchant accounts?Sub-balances (N buckets) summed on read, swept hourly7.4
Q-04Example: Idempotency key retention?7 days for transfers, keyed per client and endpoint7.3
Q-05Example: Limit checks synchronous?Yes for hard limits; velocity scoring asynchronous with hold-for-review7.6
[Q-nn][Question][Option][Section]

1.4 45-minute interview path

MinuteCoverNotes
0 to 5Clarify scope: P2P only or merchants too, top-up and withdrawal, currenciesState the invariants in one sentence each (2.2)
5 to 10Core requirements and below-the-line listPush FX, cards and credit lines below the line
10 to 15Estimates with consequences (3.5)Show why one database is not enough and why hot accounts exist
15 to 20API with idempotency key; ledger data model with the access-pattern tableWrite the entry and account rows on the board
20 to 30High-level diagram, then the main deep dive: cross-shard transfer (7.2)Compare 2PC, saga and event sourcing; pick one and defend it
30 to 40Second deep dive: idempotency (7.3) or hot accounts (7.4)Choose hot accounts if the interviewer pushes on scale
40 to 45Reconciliation, failure modes, what changes at 10xEnd on how you would know money is wrong

Pick 7.2 as the default deep dive. Mention 7.5 (reconciliation) even if there is no time to detail it: a wallet without reconciliation is not credible.

2. Requirements

2.1 Core functional requirements

IDRequirementPriority
FR-01Example: Show a customer's available and pending balance per currencyMust
FR-02Example: Transfer funds between two wallets (P2P) with a noteMust
FR-03Example: Pay a merchant wallet from a customer walletMust
FR-04Example: Top up from a linked bank account or card through a providerMust
FR-05Example: Withdraw to a linked bank accountMust
FR-06Example: Show a statement of transactions with running balanceMust
FR-07Example: Apply per-tier limits (single transfer, daily, monthly, balance cap) tied to KYC levelMust
FR-08Example: Reverse a completed transfer through a new compensating transfer, never by editing historyMust
FR-09Example: Hold funds for review when risk scoring requests itShould
[FR-nn][Requirement][Must / Should / Could]

2.2 Non-functional requirements and invariants

IDQualityTargetMeasured as
NFR-01Ledger balanceExample: Sum of all entries per currency equals zero at every commitContinuous invariant check
NFR-02No negative balanceExample: No account below its allowed floor (0 unless a credit line exists)Constraint in the debit predicate; nightly audit
NFR-03Exactly-once effectExample: Zero transfers applied twice for the same idempotency keyDuplicate-key audit
NFR-04Transfer latencyExample: p99 at or below 400 ms for same-shard, 1.5 s for cross-shard completionService timing
NFR-05AvailabilityExample: 99.95% monthly for transfers; balance reads 99.99%SLO
NFR-06DurabilityExample: RPO 0 for committed ledger entriesSynchronous replication
NFR-07ReconciliationExample: Every external movement matched within 1 business day; breaks investigated within 2Reconciliation dashboard
NFR-08AuditabilityExample: For each transfer, the limit set, rule version and KYC tier in force are retrievable for [retention period]Audit query
[NFR-nn][Quality][Target][Measurement]

2.3 Below the line (out of scope)

  • [Example: foreign exchange between wallet currencies (separate FX design)]
  • [Example: card issuing linked to the wallet]
  • [Example: credit lines and overdrafts]
  • [Example: interest or yield on balances]
  • [Example: the card acquiring ledger (see Payment Ledger System Design (system-design/design-payment-ledger))]
  • [Example: fraud model internals (see Payment Fraud Detection System Design (system-design/design-fraud-detection))]
  • [Item deferred, with the release that will take it]

3. Capacity estimation

3.1 Assumptions

InputValueSource
Active walletsExample: 50 millionProduct forecast
Transfers per active wallet per dayExample: 4Current usage
Peak to average ratioExample: 5Lunch and payday peaks
Ledger entries per transferExample: 4 (two legs plus fee and in-flight legs on average)7.1 and 7.2
Entry row size including indexesExample: 250 bytesSchema in 5.1
Share of transfers crossing shardsExample: about 0.98 with 64 shards (63/64)Random account placement
Credits per second to the largest merchant at peakExample: 2 000Merchant analytics

3.2 Throughput

QuantityCalculationResultConsequence
Transfers per day50 million x 4200 millionAbove what one primary handles comfortably for writes with indexes
Average transfers per second200 million / 86 400about 2 300/sBaseline for normal provisioning
Peak transfers per second2 300 x 5about 11 600/s, round to 12 000/sShard the ledger from day one
Peak entry writes12 000 x 448 000 entries/sWith Example 3 000 entries/s per shard at safe utilisation, at least 16 shards; choose 64 for growth

3.3 Storage

QuantityCalculationResultConsequence
Entries per day200 million x 4800 millionPartition entry tables by account and by month
Bytes per day800 million x 250 B200 GB/dayHot storage for 13 months, then archive
Bytes per year200 GB x 365about 73 TB/yearArchive tier with retrieval for audits; statements read from monthly snapshots

3.4 Contention

The largest merchant at 2 000 credits/s would serialise on one balance row. If a row update with commit takes Example 2 ms under lock, one row sustains at most about 500 updates/s, so the merchant needs at least 4 sub-balances; choose 16 to absorb bursts (7.4).

3.5 Summary

QuantityEstimateDesign consequence
Peak transfers12 000/sSharded ledger, 64 shards
Cross-shard shareabout 98%Cross-shard transfer is the normal path, not an edge case; design it first (7.2)
Ledger growth73 TB/yearPartition by month; monthly balance snapshots for statements
Hot merchant2 000 credits/sSub-balances, not one row

4. API design

4.1 Endpoints

Method and pathPurposeIdempotent
GET /v1/wallets/{id}/balanceAvailable and pending balance per currencyYes (read)
GET /v1/wallets/{id}/transactions?cursor=Statement with running balanceYes (read)
POST /v1/transfersTransfer between walletsYes, with Idempotency-Key
GET /v1/transfers/{id}Transfer status (pending, completed, held, failed, reversed)Yes (read)
POST /v1/transfers/{id}/reverseCompensating transfer, by an operator or under policyYes, with Idempotency-Key
POST /v1/topupsPull funds from a linked instrument through a providerYes, with Idempotency-Key
POST /v1/withdrawalsPush funds to a linked bank accountYes, with Idempotency-Key

4.2 Request and response examples

POST /v1/transfers
Idempotency-Key: 4b7e0a3c-1f2d-4c8e-9a51-6d2b8e0f7c33
Authorization: Bearer [token]
Content-Type: application/json

{"from_wallet": "wal_81aa", "to_wallet": "wal_19fe", "amount": {"value": 2500, "currency": "EUR"}, "note": "Dinner"}
HTTP/1.1 202 Accepted
Content-Type: application/json

{"transfer_id": "trf_5c02", "status": "pending", "amount": {"value": 2500, "currency": "EUR"}, "created_at": "YYYY-MM-DDThh:mm:ssZ"}

Amounts are integers in the minor unit of the currency. The API never accepts floating-point amounts. A same-shard transfer may return 201 with completed; a cross-shard transfer returns 202 and completes asynchronously (7.2).

4.3 Idempotency and errors

  • The client generates a unique key per logical operation and reuses it on every retry. The server stores the key, a hash of the request body, the owning client and the final response for 7 days.
  • The same key with the same body returns the stored response. The same key with a different body returns 422. A request that arrives while the first is still running returns 409 request-in-progress.
  • Errors use RFC 9457 problem details with a stable type URI.
StatusProblem typeMeaning
402insufficient-fundsAvailable balance below the amount
403limit-exceededA tier limit blocks the transfer; body names the limit, not the threshold
403kyc-requiredThe action needs a higher verification tier
409request-in-progressSame key is being processed
422idempotency-key-reusedSame key, different request
423wallet-frozenWallet is frozen by compliance or the customer

5. Data model

5.1 Entities

Account types: customer, merchant, in_flight (one per shard and currency), fees, bank_settlement (mirrors the safeguarding or partner bank account), provider_clearing (one per top-up and withdrawal provider).

5.2 Storage choices and invariants

DataStorePartition keyInvariant enforced by
Accounts, entries, sub-balancesExample: Sharded relational database, 64 shards, synchronous replica per shardaccount_idDebit predicate balance - amount >= floor; entries and balance in one transaction
TransfersSame shard as the sender accountfrom_account_idState machine in 7.2
Idempotency recordsSame shard as the sender accountclient_id, keyUnique constraint, written in the debit transaction
OutboxSame shard as the writernoneWritten in the same transaction as entries
StatementsRead replica plus monthly snapshot tableaccount_id, monthSnapshot plus entries since snapshot equals balance
Limits countersIn-memory store with durable backingwallet_id, windowRebuilt from entries if lost

5.3 Access patterns

IDQueryServed byPartition key rationale
AP-01Example: Current balance of a walletaccount row by account_id (plus sub-balances for merchants)One-shard read; hottest read path
AP-02Example: Debit sender and record entriesaccount row lock or conditional update, insert entrySender account and its entries co-located so the debit is one local transaction
AP-03Example: Statement page for a monthentry index on (account_id, posted_at)Entries for one account live on one shard
AP-04Example: Look up a transfer by IDtransfer primary key; transfer ID embeds shard numberAvoids a global index
AP-05Example: Check idempotency keyUnique index (client_id, key) on the sender shardSame transaction as the debit
AP-06Example: Daily sum of entries per settlement accountentry index on (account_id, posted_at) for settlement accountsFew accounts; feeds reconciliation
AP-07Example: Daily spend for limitsLimits counter by wallet_id and dayFast path; ledger is the fallback

6. High-level design

ComponentResponsibilityScales by
Transfer APIValidate, check idempotency, call limits and risk, start transferHorizontally
Limits and KYC serviceEvaluate tier limits, record rule version appliedHorizontally, counters partitioned by wallet
Ledger servicePost balanced entries within one shard; run saga stepsShards
Outbox relayPublish committed events exactly once per event IDOne relay per shard
External funds serviceTop-ups and withdrawals through providers; owns clearing accountsHorizontally; provider limits apply
ReconciliationInternal invariants continuously, external statements dailyBatch and streaming jobs

7. Detailed design

7.1 Double-entry ledger and balances

Every movement of value posts at least two entries whose amounts sum to zero in one currency. A transfer of EUR 25.00 from Alice to Bob on the same shard:

EntryAccountDebitCredit
E1 (Example)customer:alice2 500
E2 (Example)customer:bob2 500

Rules:

  • Entries are immutable. Corrections are new transfers that reference the original (FR-08).
  • The balance column is a projection maintained in the same transaction as the entries. A nightly job recomputes balances from entries and alerts on any difference (NFR-01).
  • A pending balance (holds for review, unsettled top-ups) is a separate account type, not a flag on entries, so available balance is always one read.

Options for the balance: compute on read from entries (always correct, slow for long histories); store a projection updated with entries (fast, needs a recompute check); store only snapshots plus recent entries (bounded read cost). Choice: stored projection plus monthly snapshots. Reason: AP-01 is the hottest path and must be one row read, while snapshots keep statements and audits cheap.

7.2 Deep dive: cross-shard transfers

About 98 percent of transfers cross shards (3.5). The sender debit and the receiver credit cannot share a local transaction.

OptionHow it worksStrengthWeakness
Two-phase commit (2PC)Coordinator prepares both shards, then commitsAtomic across shardsBlocking on coordinator failure; locks held across network round trips; poor at 12 000/s
Try-confirm-cancel (TC/C)Reserve on sender, reserve on receiver, then confirm both or cancel bothNo distributed lock; explicit compensationTwo phases of application logic per participant; more states
Saga through an in-flight accountLocal transaction 1 debits sender and credits in-flight; outbox event triggers local transaction 2 that debits in-flight and credits receiverEach step is a local ACID transaction; ledger stays balanced at every instantMoney is visibly in flight for a short time; needs compensation if step 2 cannot apply
Event sourcing with deterministic replayTransfer commands appended to a partitioned log; state machines per partition apply them in order; snapshots for speedFull history, replayable, high throughputHarder queries, harder schema evolution, a new operating model for the team

Choice: saga through an in-flight account. Reason: it keeps the ledger balanced at every commit (the in-flight account carries the value), uses only local transactions, and fits the team's relational operating model. Event sourcing remains the path if throughput grows beyond what sharding can absorb (10).

  • Step T2 deduplicates on event ID, so redelivery is harmless.
  • The in-flight account per shard must net to zero once all sagas settle; a non-zero in-flight balance older than Example 5 minutes is an alert.
  • The sender sees the debit immediately; the receiver sees the credit when T2 commits. The UI shows "sent" after T1 and "received" after T2.

7.3 Deep dive: idempotency

OptionStrengthWeakness
Client-generated key stored with responseSimple, standard, survives client retriesRequires storage and a retention policy
Natural key (client reference) unique per accountNo extra tableCouples API to client data shape
Deduplicate in a cache onlyFastCache loss means double spend

Choice: client-generated key stored in the sender shard in the same transaction as T1. Reason: the key and the debit commit or fail together, so a retry after a timeout finds either nothing (safe to apply) or the stored response (return it). Internal saga steps reuse transfer_id:step as their key, and calls to external providers pass a derived key so providers also deduplicate.

How real systems do it (paraphrased from a public write-up; not copied)

Stripe's engineering post on idempotency describes the client side of the contract: a client creates a unique key for each operation it wants to happen once, attaches it to every attempt, and on a network failure retries with the same key, so the server can answer with the result of the first attempt instead of acting again. The post also recommends retrying with exponential backoff plus random jitter so that many clients recovering from the same outage do not hit the server in lockstep. This design adopts both ideas: the wallet SDK owns key generation and the retry schedule, and the server treats the key as part of the transaction.

Source: Brandur Leach, "Designing robust and predictable APIs with idempotency", Stripe blog, 22 February 2017, https://stripe.com/blog/idempotency

7.4 Deep dive: hot accounts

OptionHow it worksUse here
One row, row lockEvery credit locks the merchant balanceFails above about 500 updates/s (3.4)
Sub-balancesMerchant account split into N buckets; a credit picks a bucket by hash of transfer ID; balance read sums bucketsChosen for merchants above a threshold
Batched creditsCredits accumulate in a queue and post every second as one entry pair per batchUse for payout-only merchants that do not read balance in real time
Credit-only appendNo balance update on credit, only entries; balance computed by periodic roll-upFallback for extreme bursts

Choice: sub-balances with Example 16 buckets for accounts flagged hot. Reason: credits never need to lock the whole account, debits (payouts) are rare and can lock all buckets briefly, and the ledger remains exact. An hourly sweep moves bucket balances into bucket 0 to keep reads cheap.

7.5 Reconciliation

LevelComparesFrequencyBreak action
L1 internalSum of entries per currency is zero; projections equal recomputed balancesContinuous and nightlyPage on any non-zero
L2 sagaIn-flight accounts net to zero after settlement windowEvery 5 minutesInvestigate stuck sagas
L3 providerProvider clearing account entries against provider settlement filesDailyMatch by provider reference; open a break ticket
L4 bankSum of customer balances against safeguarding or partner bank balanceDailyTreasury review; regulatory report if required

Every break has an owner, an age and a resolution code. Resolution is always a new adjusting transfer approved by two people, never an edit.

7.6 Limits and KYC hooks

  • Each wallet has a KYC tier. Each tier maps to a limit set (single transfer, daily, monthly, maximum balance). Limit sets are versioned configuration with an approver.
  • The limits service is called synchronously before T1 with the wallet, amount and counterparty. It returns allow, deny(limit_id) or step_up(required_tier). The transfer records the limit set version (NFR-08).
  • Risk scoring runs in parallel; a review verdict sends funds to a pending account instead of the receiver and opens a case.
  • Sanctions screening and suspicious activity reporting are owned by compliance systems. The wallet emits events and accepts freeze orders (status frozen, error 423) through an authenticated internal API.

7.7 Regulatory notes (format only)

Record, for your jurisdiction, in your own words and with links to the current consolidated texts: the licence or programme under which balances are held; the safeguarding method for customer funds; KYC tiers and the evidence each needs; record retention periods; complaint and dispute handling duties; and the reports the reconciliation in 7.5 must feed. This section is a placeholder for that analysis, not the analysis itself.

Obligation areaSource (jurisdiction, article or section)Design elementOwner
[Safeguarding of funds][Law, article]7.5 L4[@name]
[Customer due diligence tiers][Law, article]7.6[@name]
[Record retention][Law, article]5.2, 9.3[@name]

8. Scalability and performance

ConcernApproachLimit before redesign
Ledger write rate64 shards; add shards by splitting ranges of account_idExample: 3 000 entries/s per shard at 60% utilisation
Cross-shard latencyOutbox relay polling at 50 ms or log-based change captureRelay lag p99 under 500 ms
Hot accountsSub-balances, sweepBucket count per account
Statement readsRead replicas, monthly snapshotsReplica lag under 1 s
Limits checksIn-memory counters, rebuilt from ledgerCounter store memory

Latency budget for a same-shard transfer (p99 target 400 ms):

StepBudget
Auth and validation20 ms
Limits service (40 ms) and risk scoring (60 ms) in parallel60 ms
T1 transaction with idempotency and outbox60 ms
Network and response40 ms
Headroom for retries220 ms

9. Reliability, security and operations

9.1 Failure modes

IDComponentFailureDetectionBlast radiusMitigationRTO
FM-01Ledger shard primaryCrash or failoverHealth check, replication alarmsWallets on one shard (1/64)Synchronous replica promotion; idempotency keys absorb client retriesExample: 2 min
FM-02Outbox relayStops publishingOutbox ageCross-shard transfers from one shard stay pendingRestart relay; T2 dedupes on replayExample: 5 min
FM-03Limits serviceUnavailableError rateAll transfersFail closed for transfers above Example EUR 100; fail open with post-check belowExample: 5 min
FM-04ProviderTimeout on top-upStep timeoutTop-ups via that providerQuery by idempotency key before retry; second providerExample: 15 min
FM-05Reconciliation jobFails or lagsJob heartbeatDetection only, not movementRerun; freeze withdrawals if L4 is over 2 days staleExample: 4 h
FM-06Clock skewEntries posted with wrong timeSkew monitorStatements and limits windowsServer time from one source; windows on posting sequenceExample: 30 min
[FM-nn][Component][Failure][Signal][Scope][Mitigation][RTO]

Score these in a Software FMEA (ops/fmea-software) and rehearse FM-01 and FM-02 with a Chaos Experiment Plan (ops/chaos-experiment).

9.2 Security and abuse

IDThreatControl
SEC-01Account takeover drains a walletStep-up authentication for new device, new payee and large amounts; cooling-off on new payees
SEC-02Insider edits a balanceNo update path for balances outside the ledger service; adjustments need two approvers; database access audited
SEC-03Replay of a signed transfer requestIdempotency key plus request timestamp window
SEC-04Money mule networksRisk scoring on graph features; limits by tier; case management
SEC-05Enumeration of wallet IDsOpaque IDs; payee lookup rate-limited and requires contact match
[SEC-nn][Threat][Control]

Model the rest in a Threat Model (security/threat-model).

9.3 Observability

SignalMetricAlert threshold
Ledger balanceSum of entries per currencyAny non-zero
In-flight ageOldest unsettled in-flight entryOver 5 min
Duplicate keysSame key, different body (422) rateDoubling vs baseline
Reconciliation breaksOpen breaks by ageAny older than 2 business days
Transfer latencyp99 by same-shard and cross-shardOver NFR-04 for 10 min

9.4 Cost estimate

ItemUnit costQuantityMonthly cost
Ledger shards with replicas[USD per node-month]Example: 128 nodes[USD]
Event log[USD per broker-month]Example: 6 brokers[USD]
Archive storage[USD per TB-month]Example: 73 TB per year[USD]
Total[USD]

10. Trade-offs and alternatives

DecisionOptionsChosenWhyWhat we give up
Cross-shard transfer2PC; TC/C; saga; event sourcingSaga through in-flight accountLocal transactions only, ledger always balancedShort visible in-flight state
Balance storageComputed; projection; snapshotsProjection plus snapshotsOne-row readsRecompute job and drift alert
Hot accountsRow lock; buckets; batchingBucketsExact and parallelReads sum N rows
Idempotency storeCache; databaseDatabase, same transactionNo double spend on cache lossStorage cost
Limits failureFail open; fail closedThreshold-basedSmall payments keep workingSome risk below threshold
At 10x (120 000 transfers/s)More shards; event-sourced ledgerMove to event-sourced partitions with deterministic applyShard count and relay fan-out become the limitRewrite of ledger service and tooling
At 0.1x (1 200 transfers/s)One primary; shardsOne primary with replica, same schemaCross-shard saga unnecessary; keep in-flight pattern for external flowsRe-sharding later is a migration
[Decision][Options][Chosen][Why][Cost]

11. Delivery plan and risks

11.1 Milestones

MilestoneScopeExit criterionTarget date
M1Ledger service, single shard, idempotencyL1 reconciliation clean for 30 days in shadow modeYYYY-MM-DD
M2Sharding and cross-shard sagaChaos test of FM-01 and FM-02 with zero imbalanceYYYY-MM-DD
M3Limits and KYC hooks with versioned rulesCompliance sign-off on audit queryYYYY-MM-DD
M4Provider and bank reconciliation14 consecutive days with breaks under targetYYYY-MM-DD
M5Hot-account bucketsLoad test at 2x largest merchant peakYYYY-MM-DD

11.2 Migration and rollback

[Example: Run the new ledger in shadow mode, posting the same transfers as the old balance table and comparing balances hourly. Cut over wallets in cohorts by shard. Rollback for a cohort before cutover is a routing switch; after cutover, the old table is rebuilt from new ledger entries.]

11.3 Risks

IDRiskLikelihoodImpactMitigationOwner
R-01Example: Migration mismatch between old balances and opening entriesMediumHighOpening balance transfer per wallet signed off by finance[@name]
R-02Example: Regulator questions in-flight funds in safeguarding calculationLowHighTreat in-flight as customer funds in L4[@name]
R-03Example: Provider settlement file format changesMediumMediumSchema checks, Data Contract (ai-data/data-contract) with provider feeds[@name]
[R-nn][Risk][L / M / H][L / M / H][Mitigation][@name]

12. Appendices

12.1 Traceability

RequirementDesign elementVerification
NFR-017.1 balanced entries, 7.5 L1Continuous invariant check
NFR-025.2 debit predicateProperty-based tests; nightly audit
NFR-037.3 idempotencyFault injection with client retries
NFR-077.5 L3 and L4Reconciliation dashboard
NFR-087.6 rule version on transferAudit query test
[ID][Element][Test]

12.2 Open questions

IDQuestionOwnerDue
OQ-01Example: Do we allow transfers to unregistered phone numbers with a claim window?[@name]YYYY-MM-DD
OQ-02Example: Bucket count per merchant: static or adaptive?[@name]YYYY-MM-DD
[OQ-nn][Question][@name]YYYY-MM-DD

12.3 Glossary

TermMeaning
Double-entryEvery movement posts entries that sum to zero
In-flight accountInternal account that holds value between the steps of a cross-shard transfer
Hot accountAccount receiving more updates than one row can serialise
Idempotency keyClient-generated identifier that makes retries safe
Reconciliation breakA difference between two records that should agree
SafeguardingKeeping customer funds separate from the operator's own funds
KYC tierLevel of customer verification that unlocks a limit set

12.4 References

ReferenceUse
Brandur Leach, "Designing robust and predictable APIs with idempotency", Stripe blog, 2017Idempotency contract and retry behaviour (7.3)
Alex Xu and Sahn Lam, System Design Interview: An Insider's Guide, Volume 2, ByteByteGo, 2022, chapter "Digital Wallet"Inspiration for problem framing only; no text or numbers reused
RFC 9457 Problem Details for HTTP APIsError format (4.3)
IETF draft "The Idempotency-Key HTTP Header Field" (draft-ietf-httpapi-idempotency-key-header-07, expired)Header semantics; not a standard
Garcia-Molina and Salem, "Sagas", ACM SIGMOD 1987Saga pattern (7.2)
Payment Ledger System Design (system-design/design-payment-ledger)Card acquiring ledger and PSP states
Payment Fraud Detection System Design (system-design/design-fraud-detection)Risk scoring called in 7.6
Distributed Message Queue System Design (system-design/design-message-queue)Event log and delivery semantics behind the outbox
Event Sourcing and CQRS Design (architecture/event-sourcing-cqrs)The 10x alternative

Standard and licence note

Based on common industry practice for double-entry ledgers and stored-value wallets and on the public references in 12.4. Books and company blogs were used as inspiration only; the text, diagrams and numbers are original and illustrative. The Idempotency-Key header field is an IETF draft, not a published RFC. Regulatory content is a documentation format, not legal advice: check the current consolidated texts and licence conditions for your jurisdiction. Template licence: MIT.

Modèles associés