Wallets, Payments, And Refund Ledgers
Asked of: Software Engineer
Last updated

What's being tested
This area tests whether you can design and implement financially correct transaction systems: ledgers that preserve money movement history, refund allocation logic that is deterministic and auditable, and payment workflows that survive retries, partial failures, and currency edge cases. Airbnb cares because bookings involve guest charges, host payouts, service fees, taxes, cancellations, disputes, coupons, credits, and refunds across many payment methods and currencies. The interviewer is probing for disciplined engineering: append-only records instead of mutable balances, explicit invariants, idempotency, ordering rules, and a clear separation between payment processor state and Airbnb’s internal source of truth. Strong answers combine system design with concrete algorithms for allocating refundable amounts across prior payments.
Core knowledge
-
Append-only ledger design is the foundation. Never model money by overwriting a single balance column as the source of truth; store immutable ledger entries with
account_id,currency,amount,direction,transaction_id,created_at, and metadata. Balances are derived as . -
Double-entry accounting means every movement has equal and opposite entries, so total system balance remains conserved. For example, guest payment capture debits
GuestReceivableand creditsAirbnbCash; a refund reverses cash out and guest liability. Invariant: total debits must equal total credits per transaction. -
Idempotency keys prevent duplicate charges, refunds, and ledger postings during retries. Use a client- or workflow-generated key such as
refund_request_id, enforce uniqueness inPostgres, and return the original result on replay.Stripe’s common pattern is: same idempotency key plus same parameters equals same response. -
Refund allocation is usually a deterministic greedy algorithm over refundable payment records. Filter payments with remaining refundable balance, group by method or processor constraints, sort by priority and recency, then allocate
min(remaining_refund, payment_remaining). Complexity is typicallyO(n log n)for sorting orO(n)if inputs are already ordered. -
Refundable balance should be derived from ledger history, not trusted from stale cached fields. For a payment :
Cache it for performance only if you can rebuild and reconcile it from immutable entries. -
Prioritization rules must be explicit and stable. Common rules: refund to original payment method before wallet credit, refund promotional credits last or never as cash, prefer most recent payment first due to processor limits, and respect card-network refund windows. Add a stable tie-breaker like
payment_idto avoid nondeterministic outputs. -
Partial and multiple refunds require transaction-level state machines. A booking might be
PAID -> PARTIALLY_REFUNDED -> REFUNDED, while each payment can independently beCAPTURED,PARTIALLY_REFUNDED,REFUND_PENDING,REFUND_FAILED, orREFUNDED. Do not conflate booking state with payment-instrument state. -
Concurrency control matters when two refund requests race. Use database transactions, row-level locks such as
SELECT ... FOR UPDATE, optimistic version checks, or a serialized workflow engine. The key invariant is that total allocated refunds cannot exceed total refundable amount, even under duplicate requests and retries. -
External payment processors are not the internal ledger. Processor calls can timeout after succeeding, webhooks can arrive out of order, and refunds may settle days later. Record internal intent, call the processor idempotently, then reconcile asynchronous events into ledger entries without double-posting.
-
Holds, authorizations, captures, and payouts are different money states. An authorization reserves funds but is not cash captured; a capture moves money; a hold may reserve wallet balance; a payout sends funds to a host. Model each as separate ledger events or account transitions rather than boolean flags.
-
Multi-currency systems need currency-aware arithmetic. Store amounts in minor units, such as cents, with
currencyon every ledger row. Never sum across currencies without an FX conversion event. If converting, storesource_amount,source_currency,fx_rate,target_amount,target_currency, rounding mode, and rate timestamp. -
Rounding and allocation need deterministic residual handling. If splitting $100.00 across three parties, integer minor-unit math may leave a one-cent remainder. Assign residuals by a documented rule, such as largest remainder method or deterministic ordering, so repeated computations produce identical ledger entries.
Worked example
For Design an Airbnb wallet with holds and payouts, a strong candidate first clarifies scope: “Are we designing the internal wallet ledger, payment processor integrations, or both? Do we need multi-currency? Are wallet balances spendable by guests, payable to hosts, or both? What consistency guarantees are required for double-spend prevention?” Then declare assumptions: one wallet per user per currency, append-only ledger as the source of truth, and Postgres transactions for the core write path.
The answer can be organized around four pillars. First, define the data model: wallet_account, ledger_transaction, ledger_entry, hold, payout, and idempotency_key. Second, describe core flows: add funds, place hold for booking, capture hold, release hold, refund, and host payout. Third, explain correctness mechanisms: double-entry postings, row locks on wallet accounts or holds, idempotent APIs, and reconciliation against processor webhooks. Fourth, cover operational concerns: balance materialization, audit trails, failure recovery, and alerts for ledger imbalance.
A key tradeoff is whether to calculate balances on demand from ledger rows or maintain a materialized available_balance and held_balance. On-demand reads are simpler and safer but can become expensive as ledger rows grow; materialized balances are faster but require strict transactional updates and periodic rebuild jobs. A good answer would choose materialized balances for hot reads while treating the append-only ledger as canonical.
Close by saying: “If I had more time, I’d go deeper on multi-currency FX locking, payout failure handling, and reconciliation dashboards for finance operations.” That shows you understand the boundary between core SWE design and production-grade financial reliability.
A second angle
For Allocate refund across payments, the same concept becomes more algorithmic than architectural. Instead of designing the whole wallet, focus on representing prior payments and refunds, deriving remaining refundable balances, and applying a deterministic ordering rule. A clean implementation would normalize each payment into {payment_id, method_type, captured_amount, refunded_amount, created_at, priority} and produce allocation rows {payment_id, refund_amount}. The main design constraint is correctness under partial refunds: the sum of allocations must equal the requested refund unless funds are insufficient, and no payment can be over-refunded. The interviewer may then extend it into system design by asking how this allocation is persisted, retried, and reconciled with processor responses.
Common pitfalls
Pitfall: Treating balances as mutable fields instead of ledger-derived facts.
A tempting design is a wallet_balance table that increments and decrements directly. That may pass a toy design, but it fails auditability and makes bug recovery difficult. A stronger answer says cached balances are allowed, but immutable ledger entries are canonical and can rebuild every balance.
Pitfall: Hand-waving refund ordering rules as “business logic.”
Refund prioritization is not just a product detail; it affects deterministic execution, testability, and financial correctness. Instead of saying “apply the company’s policy,” define a rule engine or ordered comparator: payment method priority, processor eligibility, recency, remaining balance, and stable tie-breaker.
Pitfall: Ignoring asynchronous processor behavior.
Many candidates assume refund() either succeeds or fails synchronously. In real payment systems, timeout does not imply failure, webhooks can be delayed, and processor state can diverge from internal state. Better answers introduce idempotent processor calls, pending states, reconciliation jobs, and exactly-once ledger posting at the application level using uniqueness constraints.
Connections
Interviewers may pivot from this topic into idempotent API design, distributed transactions and sagas, database isolation levels, event-driven reconciliation, or multi-currency pricing. They may also ask for a coding version of the same domain: implement refund allocation using sorting, grouping, and remaining-balance aggregation.
Further reading
-
Stripe API Idempotent Requests — practical reference for retry-safe financial APIs and request de-duplication.
-
Martin Fowler, Accounting Patterns — useful conceptual grounding for accounts, entries, and posting rules.
-
Designing Data-Intensive Applications — strong background on transactions, consistency, replication, and failure modes relevant to ledgers.
Featured in interview prep guides
Practice questions
- Implement prioritized refund allocation engineAirbnb · Software Engineer · Technical Screen · Medium
- Design an Airbnb wallet with holds and payoutsAirbnb · Software Engineer · Technical Screen · hard
- Design and implement an Airbnb walletAirbnb · Software Engineer · Technical Screen · medium
- Design multi-currency pricing end-to-endAirbnb · Software Engineer · Onsite · hard
- Allocate refund across paymentsAirbnb · Software Engineer · Onsite · Medium
- Design refundable transaction ledger and prioritization rulesAirbnb · Software Engineer · Onsite · Medium
Related concepts
- Banking Ledgers And Cashback OperationsSystem Design
- Payment Processing And Ledger SystemsSystem Design
- Payment Systems: Ledgers, Idempotency, and Reconciliation
- Donation And Payment PlatformsSystem Design
- Distributed System Design For Ledgers And CountersSystem Design
- Money-Safe Financial ComputationCoding & Algorithms