Double-Entry Accounting
A digital wallet is a ledger, and the correct foundation for any ledger is double-entry accounting. Every financial event creates exactly two ledger entries: a debit on one account and a credit on another. A transfer of $50 from Alice to Bob creates a debit entry on Alice’s account and a credit entry on Bob’s account. The two entries always sum to zero; money is conserved by construction.
Ledger entries are immutable. You never update or delete an entry; you create a compensating entry to reverse a prior transaction. This gives you a complete, auditable history of every financial event in the system. Balance at any point in time is computable as the sum of all credits minus the sum of all debits for an account up to that timestamp — useful for disputes, regulatory audits, and reconciliation.
The ledger table schema: entry_id, account_id, transaction_id, amount, type (debit|credit), created_at. The transaction table links paired entries: transaction_id, from_account, to_account, amount, currency, status, idempotency_key, created_at. Every financial operation in the system writes through this model, including fee collection, currency conversion spread, and interest accrual.
Balance Management
Computing balance by summing the full ledger on every read is too slow for accounts with years of history. Instead, maintain a materialized balance in a balance_cache table: account_id, currency, available_balance, pending_balance, updated_at. The cache is updated atomically within the same database transaction that writes the ledger entries, so it never diverges from ground truth under normal operation.
For the hot path — balance checks before authorizing a transaction — read the cached balance from Redis with sub-millisecond latency. Write-through caching: the DB transaction updates both the database balance_cache row and the Redis key. Cache TTL is set long (hours) since the authoritative update always goes through the cache-busting write path.
Periodic reconciliation (every few hours) recomputes balance from ledger and compares against balance_cache. Any discrepancy pages on-call immediately. In practice, discrepancies indicate a bug in the transaction write path — a ledger write succeeded but the balance update was skipped, or vice versa. Reconciliation also catches any data corruption from hardware failures or bugs in compensating transaction logic.
Transaction Atomicity
A money transfer must be atomic: both the debit and the credit succeed, or neither does. Implement this with a database transaction wrapping all ledger writes for a given transfer. The sequence: BEGIN; validate sender balance; INSERT debit entry; INSERT credit entry; UPDATE balance_cache for both accounts; COMMIT. If any step fails, the database rolls back all writes and the balances are unchanged.
Idempotency keys prevent duplicate processing. Each transfer request from a client includes a client-generated idempotency_key (UUID). Before executing the transfer, check if a transaction with this key already exists. If it does, return the existing result without re-executing. Idempotency keys are stored with the transaction record and indexed for fast lookup. Keys expire after 24 hours to bound storage.
Distributed scenarios — where the wallet service calls an external payment network — require a two-phase approach: write a PENDING transaction record, execute the external call, then mark the transaction COMPLETED or FAILED based on the response. A background job scans for stuck PENDING transactions older than a timeout and reconciles them against the external network’s status API. This handles the case where the response is lost after a successful external operation.
Transfer Flow
A P2P transfer between two wallet accounts: (1) Client sends transfer request with idempotency key. (2) Service validates: sender account exists and is active, sufficient available balance, recipient account exists and is active, transfer amount within daily/monthly limits. (3) Service creates a PENDING transaction record. (4) Database transaction: debit sender, credit receiver, update balance cache for both, mark transaction COMPLETED. (5) Async: enqueue push notification to sender and recipient, enqueue email receipt, publish transaction event to event bus for downstream consumers (fraud monitoring, reporting, loyalty).
Cross-wallet transfers — to a different wallet provider or bank account — go through an external payment rail (ACH, wire, instant payment network). The transfer is PENDING until the external network confirms settlement. Funds are debited from the sender immediately and held in a suspense account; the recipient is credited when settlement confirms. If the external transfer fails, the suspense account is debited and the sender is credited back.
Transfer limits are enforced at multiple layers: per-transaction limit, daily limit, monthly limit, and velocity limit (count of transfers per hour). Limits are configurable per account tier based on KYC status. Limit checks read from Redis for performance and are enforced under an account-level advisory lock to prevent concurrent transfers from bypassing the daily limit check.
Currency Conversion
Exchange rates are fetched from a provider (e.g., Open Exchange Rates, Wise, or a bank feed) and cached in Redis with a 30-second TTL. On a cross-currency transfer, the rate at time of transaction is locked in and recorded on the transaction record for auditability. The sender sees the exact exchange rate and converted amount before confirming — regulatory requirement in most jurisdictions.
A spread is applied to the mid-market rate as revenue: e.g., 1.5% above mid-market for retail conversions. The spread is recorded as a separate fee ledger entry, so revenue from currency conversion is tracked separately from transfer volume. Large conversions (above a configurable threshold) are flagged for rate desk review — the system may need to hedge the position before executing at scale.
Disputed conversions require storing the rate at time of transaction indefinitely (or for the regulatory retention period). The rate history table stores: currency pair, rate, spread, source, timestamp. This table is append-only and never updated. Disputes are resolved by looking up the rate that was in effect at the transaction timestamp.
KYC Compliance
Know Your Customer (KYC) is a regulatory requirement for financial services. On account creation, collect: legal name, date of birth, residential address, government ID type and number. A third-party identity verification service (Jumio, Onfido, Persona) performs: document scan, OCR extraction of ID fields, liveness check via selfie, and match of selfie to ID photo. Results are returned via webhook and stored on the account record.
KYC status gates transaction limits. Unverified accounts: $1,000/month total transaction volume, no international transfers. Basic verified (ID confirmed): $10,000/month. Enhanced due diligence (EDD, for high-value customers or high-risk jurisdictions): unlimited with ongoing monitoring. EDD requires additional source-of-funds documentation and ongoing periodic re-verification.
Document retention follows regulatory requirements: identity documents retained for 5 years after account closure in most jurisdictions, 7 years for AML-relevant records. Documents are stored encrypted in object storage with access logged. Access to raw identity documents requires elevated permissions and is audited. De-identified attributes (name hash, DOB, ID number hash) are used for analytics and matching without exposing raw PII.
Regulatory Reporting
Anti-Money Laundering (AML) transaction monitoring runs as a stream processor over the transaction event bus. Patterns detected: structuring (multiple transactions just below the $10,000 CTR reporting threshold within a short window — a deliberate pattern to avoid reporting); layering (rapid movement of funds through multiple accounts); unusual cash-out after a period of dormancy; transactions with high-risk jurisdictions on OFAC sanctions lists.
When suspicious activity is detected, a SAR (Suspicious Activity Report) is filed with FinCEN (US) or equivalent authority within 30 days of detection. The filing is handled by the compliance team; the system generates a draft SAR pre-populated with transaction details, account history, and the specific AML pattern that triggered it. SAR filings are confidential — the account holder must not be notified that a SAR was filed.
1099-K reporting to the IRS is required for US accounts that receive over $600/year in payments (post-2022 threshold). The reporting pipeline computes annual totals per account from the ledger, generates 1099-K records, files electronically with the IRS via the FIRE system, and delivers copies to account holders by January 31. Data retention for all regulatory submissions is 7 years minimum.
Dispute and Chargeback
A user disputing a transaction triggers a case in the dispute management system. The disputed transaction is placed on hold (funds reserved, not accessible to recipient). The case record captures: transaction details, user’s dispute reason, any evidence submitted by the user. Dispute reasons are standardized: unauthorized transaction, item not received, item not as described, duplicate charge, processing error.
For disputes on wallet-to-wallet transfers, the merchant (recipient) is notified and given 7 days to submit rebuttal evidence. The disputes team reviews evidence from both parties and makes a decision within 10 business days (regulatory maximum in many jurisdictions). If the dispute is upheld, the transfer is reversed: debit the recipient’s account (or suspense if insufficient funds), credit the sender’s account, insert compensating ledger entries. A chargeback fee is charged to the losing party.
Chargeback rate per merchant is monitored. A merchant exceeding a 1% chargeback rate in a rolling 30-day window triggers a risk review and potential account suspension. Merchants with persistent high chargeback rates are terminated from the platform — payment networks impose fines on wallet providers who allow high-chargeback merchants to operate unchecked.
{ “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ { “@type”: “Question”, “name”: “How do you use double-entry accounting to maintain wallet balance integrity?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Every monetary event creates two ledger entries that sum to zero: a debit on the source account and a credit on the destination. Never mutate a balance column directly; instead derive it by summing ledger rows for that account. This makes the balance an auditable projection, not a mutable field. Store entries in an append-only ledger table with (account_id, amount, currency, transaction_id, created_at) and add a check constraint that no transaction’s entries leave the books unbalanced. Periodic reconciliation jobs verify that sum(debits) == sum(credits) across all accounts.” } }, { “@type”: “Question”, “name”: “How do you implement atomic money transfers to prevent partial writes?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Wrap the debit and credit ledger inserts in a single database transaction. To prevent deadlocks when two transfers touch the same pair of accounts simultaneously, always acquire row locks in a canonical order (e.g., lower account_id first). For cross-service or cross-shard transfers, use the Saga pattern with compensating transactions: each step publishes an event; if a downstream step fails, a rollback event triggers the compensating debit or credit. Use idempotency keys on every transfer request so retries don’t double-apply.” } }, { “@type”: “Question”, “name”: “How do you design KYC tiers with corresponding transaction limits?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Define tiers (e.g., Tier 0: email only, Tier 1: phone + SSB4, Tier 2: government ID, Tier 3: enhanced due diligence) each with daily send, receive, and cumulative balance limits stored in a policy table. Attach the verified tier to the user record after each KYC step completes. At transaction time, fetch the user’s tier, query the limit policy, and compare against rolling aggregates computed from the ledger. Make limits configurable per currency and jurisdiction without code changes. Tier upgrades are asynchronous—KYC vendor webhooks update the tier after verification clears.” } }, { “@type”: “Question”, “name”: “How do you design AML structuring detection and SAR filing workflow?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Structuring (breaking large transfers into sub-threshold chunks to evade reporting) is detected by aggregating transaction amounts over a rolling 24-hour or 10-day window per user and flagging patterns that approach reporting thresholds (e.g., multiple transactions just under $10,000). Feed these signals into a case management queue. A compliance analyst reviews flagged cases, adds narrative, and files a Suspicious Activity Report (SAR) via FinCEN’s BSA E-Filing system within the required 30-day window. The workflow system must enforce SLA timers, maintain an immutable audit trail of analyst actions, and never alert the subject (no tipping-off rule).” } }, { “@type”: “Question”, “name”: “How do you handle currency conversion at transaction time with rate locking?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “When a user initiates a cross-currency transfer, fetch a quote from your FX provider, store (rate, expiry_timestamp, quote_id) in a quotes table, and present the locked rate to the user. If the user confirms within the TTL (typically 30–60 seconds), execute the transfer using that quote_id; the ledger records both the source amount in the original currency and the destination amount in the target currency. If the quote expires, prompt for a fresh quote. Hedge your net FX exposure by periodically netting open positions and placing offsetting trades with your liquidity provider rather than hedging each transaction individually.” } } ] }See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems
See also: Coinbase Interview Guide