This question evaluates a candidate's competency in designing financial transaction systems, covering double-entry append-only ledgers, holds/authorizations and captures, refunds and payouts, multi-currency FX conversion and rounding, idempotency, concurrency safety, and auditable reconciliation.
Design and implement an Airbnb-style wallet system. Requirements:
(
1) create user accounts and allow deposits;
(
2) support booking holds/authorizations and later captures at check-in;
(
3) allow hold release on cancellation;
(
4) process partial/full refunds;
(
5) pay out hosts after platform fees and taxes;
(
6) enforce spendable vs. total balance with no negative spendable balance;
(
7) support multi-currency balances with FX conversion and rounding rules;
(
8) ensure idempotent operations via client-supplied idempotency keys;
(
9) guarantee concurrency safety for duplicate or overlapping requests;
(
10) maintain an append-only auditable ledger and derive balances from it;
(
11) provide reconciliation to detect inconsistencies;
(
12) define persistence schema (tables or classes) and transaction isolation choices;
(
13) expose clear APIs/methods and include a minimal main() to run sample scenarios;
(
14) write unit tests for happy paths and edge cases (insufficient funds, expired holds, double-capture attempts, partial refunds, FX rate changes). Specify core classes (e.g., Wallet, Account, Transaction, Hold, Capture, Refund, Payout, LedgerEntry, FXConverter), their relationships, and method signatures, and analyze time/space complexity of critical operations.
Quick Answer: This question evaluates a candidate's competency in designing financial transaction systems, covering double-entry append-only ledgers, holds/authorizations and captures, refunds and payouts, multi-currency FX conversion and rounding, idempotency, concurrency safety, and auditable reconciliation.
Design and Implement an Airbnb-Style Wallet (Ledger, Holds, FX, Idempotency)
Problem
Design and implement an Airbnb-style wallet system. This is an OOD/system-design problem with a strong implementation component: the interviewer expects runnable code (you write your own main() — no template is provided) plus the design reasoning behind it.
A guest deposits money into a wallet, a booking places a hold (authorization) on funds, that hold is captured at check-in (fully or partially) or released on cancellation/expiry, captured funds may be refunded (partially or fully), and the host is paid out the net after platform fees and taxes. Balances are multi-currency with explicit FX conversion, every operation must be idempotent and concurrency-safe, and the source of truth is an append-only, double-entry ledger from which balances are derived.
Your design must satisfy these requirements:
Create user accounts and allow deposits.
Support booking holds/authorizations and later captures at check-in.
Allow hold release on cancellation or expiry.
Process partial/full refunds.
Pay out hosts the net amount after platform fees and taxes.
Enforce spendable vs. total balance, with
no negative spendable balance
.
Support multi-currency balances with FX conversion and deterministic rounding rules.
Ensure idempotent operations via client-supplied idempotency keys.
Guarantee concurrency safety for duplicate or overlapping requests.
Maintain an
append-only auditable ledger
and derive balances from it.
Provide reconciliation to detect inconsistencies.
Define the persistence schema (tables or classes) and transaction-isolation choices.
Expose clear APIs/methods and include a minimal
main()
running sample scenarios.
Write unit tests for happy paths and the edge cases: insufficient funds, expired holds, double-capture attempts, partial refunds, and FX-rate changes.
Specify the core classes (e.g. Wallet/Account, Transaction/LedgerEntry, Hold, Capture, Refund, Payout, FXConverter), their relationships and method signatures, and analyze the time/space complexity of the critical operations.
Constraints & Assumptions
Amounts are integers in
minor units
; a currency metadata table records fraction digits (USD=2, JPY=0).
Each user wallet has
per-currency sub-accounts
; the platform, hosts, and the external world are also modeled as accounts.
The
double-entry ledger is append-only
and is the source of truth; a materialized balances cache is maintained
inside the same transaction
for fast reads.
Hold and capture happen
in the booking currency
; a guest who lacks that currency must
convert
first, so capture never needs FX.
"Spendable" = freely usable cash; "Held" = reserved by an active authorization; "Total" = spendable + held.
Scale is moderate: on the order of
1M
bookings/day → a few million ledger entries/day, well within a single tuned relational primary. The hard part is
correctness under concurrency
, not write volume.
Out of scope (call out, don't build): PSP/bank-rail integration, fraud/chargebacks/KYC, the booking service itself, and the full host-clawback flow for refunds arriving after escrow is already paid out.
Clarifying Questions to Ask
Is a partial capture allowed against a single hold, and can a hold be captured more than once (up to the authorized amount)?
On refund, do we also reverse the platform
fee and tax
, or only the guest-facing principal? Is this a policy switch?
What happens to a refund when escrow has
already been paid out
to the host — do we pre-fund a reserve, delay payout past the refund window, or claw back from the host?
Do conversions ever refund
FX slippage
if the rate moved, or is the rate fixed at conversion time and final?
What durability/consistency does the underlying store give us (single primary vs. replicas), and is a relational DB with strong isolation acceptable?
Are idempotency keys scoped per-operation, and how long must we retain them for safe replays?
What a Strong Answer Covers
Ledger as source of truth
: a clear,
uniform
sign convention; balances derived by summing legs; a stated per-currency zero-sum master invariant; and an explicit account taxonomy (user CASH/HOLD, host CASH, platform ESCROW/FEE/TAX, external WORLD).
Correct money flows
: which accounts move on deposit, hold, release, capture, payout, refund, and convert — and
why
spendable never goes negative and escrow never goes negative.
Idempotency & concurrency
: durable keys that replay the stored response, request-hash guard against key reuse, a retryable failed state, row locks in deterministic order, and unique-constraint backstops that make double-hold/double-capture impossible.
Money representation & FX
: integer minor units, single banker's-rounding at the end, captured rate for auditability, and an honest account of where multi-currency float/PnL lives.
Persistence & isolation
: a concrete schema (accounts, ledger_entries, holds, balances, idempotency_keys, fx_rates, currencies), the chosen isolation level, and the per-operation transaction pattern.
Core classes & runnable code
: well-named classes with method signatures, a working
main()
end-to-end scenario, and unit tests covering the required edge cases.
Reconciliation & operability
: a periodic job re-deriving balances from the ledger, zero-sum and non-negativity checks, and a hold-expiry sweeper.
Complexity & tradeoffs
:
O(1)
posts /
O(K)
cached reads, plus an honest discussion of hot-account (escrow) contention, cache-vs-ledger drift, and refund-after-payout.
Follow-up Questions
The platform
ESCROW
account is a write hotspot — every capture, payout, and refund locks it. How would you remove that serialization point without breaking auditability or the zero-sum invariant?
A refund arrives after the host has already been paid out and withdrawn the funds. Walk through the options (refund reserve, delayed payout, host clawback) and the compensating ledger entries each requires.
Where does the platform's
FX gain/loss
show up when it carries a multi-currency float, and why does it
not
break the per-currency zero-sum invariant?
How does reconciliation distinguish a benign balances-cache drift bug from an actual money-conservation violation, and what should each trigger operationally?