Design and Implement an Airbnb-Style Wallet (OOD + Runnable Code)
You are asked to design and implement an in-app wallet system for a marketplace like Airbnb that serves both guests and hosts. The wallet must support balances, payments (guest pays into a platform escrow when a booking is made), refunds (escrow back to the guest), and payouts (escrow to the host after the stay). Every money movement must be recorded, and errors must be handled robustly.
This is an object-oriented design exercise with a runnable bar: you will define the classes and methods, then implement a self-contained, in-memory reference in a language of your choice — including your own main/driver — that runs and passes the demo below. There is no main template provided; you write it yourself.
Concretely, the system must support:
-
Roles and accounts
-
Guest and host users, each with exactly one wallet.
-
A platform
escrow
account that holds funds between a guest's payment and the eventual payout or refund.
-
Operations
-
deposit
— external source → guest wallet.
-
pay
— guest wallet → escrow, tagged by
booking_id
.
-
refund
— escrow → guest wallet, by
booking_id
.
-
payout
— escrow → host wallet, by
booking_id
.
-
Balance lookup for any wallet and for escrow.
-
Transaction recording
-
Record every operation as an
immutable transaction
with: id, kind, amount (cents), currency, from-account, to-account, optional
booking_id
, timestamp, and optional
idempotency_key
.
-
Support listing / querying transactions by
booking_id
.
-
Error handling
— distinct, catchable errors for: insufficient funds; over-refund or over-payout beyond what the booking has in escrow; currency mismatch; invalid input (negative/zero amount, unknown user/booking); and idempotency conflicts.
-
Runnable demo
— a driver that creates a guest and host, deposits
100totheguest,pays
60 for booking
b1
, refunds
10for‘b1‘,paysout
50 for
b1
to the host, shows that an idempotent duplicate request does
not
double-apply, raises at least one error case (e.g. an over-refund), and prints final balances plus a transaction log.
Constraints & Assumptions
-
Single currency
(e.g. USD); amounts are non-negative integers in
cents
.
-
Funds are held in a
platform escrow
account between a guest's payment and the corresponding refund/payout. A booking's refunds and payouts together can never exceed what that booking placed in escrow.
-
In-memory storage
is acceptable; concurrency control can be simple (a single coarse lock that makes each operation atomic is fine, standing in for a DB transaction).
-
Clean, extensible OOD and correctness of the money invariants matter more than production infrastructure (no real card networks, KYC, taxes, FX, or durable DB required).
-
You must provide your own driver/
main
; assume no scaffolding or test harness is given.
Clarifying Questions to Ask
-
Is there a single currency, or must the design support multiple currencies and cross-currency transfers from day one?
-
Does a booking have exactly one paying guest and one settled host, and should a
refund
route back to the original payer automatically (vs. the caller supplying the guest)?
-
Can refunds and payouts be
partial
and
repeated
for the same booking (as the demo's
10refund+
50 payout against a $60 payment implies)?
-
What is the required idempotency semantic: should a replay with the
same
key and
same
parameters succeed and return the original result, while the same key with
different
parameters is an error?
-
Are negative or zero amounts always rejected, and should amounts be rejected if they aren't whole cents?
-
Is durability/persistence in scope, or is an in-memory reference with a clear note on how to make it durable sufficient?
What a Strong Answer Covers
-
Object model.
A small set of cohesive types (e.g.
User
,
Account
, immutable
Transaction
, an append-only
Ledger
, and a
WalletService
boundary), with balances living in exactly one place and every mutation flowing through one method.
-
Double-entry / conservation.
Operations expressed as paired debit/credit so total money is conserved by construction;
EXTERNAL
and
ESCROW
system accounts make deposit/pay/refund/payout the same primitive. Ability to state and check a global invariant (e.g. the signed sum of all balances is constant).
-
Per-booking escrow accounting.
Tracking how much each
booking_id
currently has in escrow so refunds/payouts are bounded and a shared escrow account can't be overdrawn by one booking spending another's funds.
-
Validation order & typed errors.
A deliberate check ordering (amount → idempotency → currency → booking ceiling → funds) and distinct exception types so callers branch precisely; rejected checks leave
zero
side effects (atomicity).
-
Idempotency.
Exactly-once retries keyed by
idempotency_key
, checked before any mutation; replay returns the original transaction, conflicting parameters raise.
-
Runnable, self-checking code.
Clean, idiomatic, dependency-free code with a driver that performs the required flow, asserts every expected balance, exercises error cases, and verifies conservation; correct final ledger and balances.
-
Scaling & production path.
How the in-memory choices map to production: DB transactions and row locks for atomicity, a
UNIQUE
idempotency constraint, a durable append-only ledger with derived-and-reconciled balances, read replicas/caching, and sharding (and the resulting hot escrow row).
Follow-up Questions
-
Why check the
per-booking escrow ceiling before
the generic insufficient-funds check? What does each ordering report when you over-refund a fully-settled booking?
-
How do you guarantee
exactly-once
money movement under client retries
and
process crashes once this is backed by a real database (not in-memory)?
-
Where do you store balances — materialized columns, or derived by summing the ledger? What are the failure modes of each, and how do you detect and repair drift?
-
The escrow account is touched by both payments and payouts. Under sharding it becomes a hot row — how would you mitigate that contention?
-
How would you extend the design to support
platform fees
on a payout,
multi-currency
with FX, and
dispute/chargeback
reversals, while keeping the conservation invariant intact?