Splitwise
"Design an expense-splitting app" packs three separate problems into one prompt: how an expense gets divided among people, how the running total of who-owes-whom is tracked, and how that tangle of pairwise debts gets collapsed into the fewest possible payments. Keeping those three cleanly apart is most of the design.
Requirements
Functional
- Add an expense: one user pays a total amount on behalf of a set of participants.
- Support three ways to split that amount: equally, by exact amounts per participant, or by percentage per participant.
- Report the net balance between any two users - who owes whom, and how much.
- Simplify debts across the group: reduce the group's tangle of IOUs to the smallest number of actual payments that settles everyone up.
Non-functional
- Adding a new split type (say, splitting by shares rather than percentages) should mean writing one new class, not editing the expense or balance-tracking code.
- Looking up the balance between two users should not require replaying every expense ever added - it should be a direct read of a maintained running total.
Design
ExpenseSplit is the one interface doing the real work: it turns "amount + participants +
however this particular split is described" into "who owes what." Expense just holds the
result and who paid; Ledger never sees a split calculation happen, it only ever
receives the finished per-person shares and folds them into running pairwise balances.
- 1The client picks which split strategy to use but never computes shares itself.
- 2The strategy alone knows whether that means dividing evenly, reading exact amounts, or applying percentages.
- 3The manager records the expense with the shares already computed.
- 4The ledger updates each participant’s running balance against the payer - it never recalculates a split.
- 5A separate request, unrelated to adding an expense.
- 6The ledger nets every user to one number and greedily matches creditors to debtors.
Keeping the split calculation, the balance bookkeeping, and the debt-simplification algorithm as three separate collaborators is what lets each one be reasoned about (and tested) without the other two in the room.
Class diagram
Code
Design decisions
ExpenseSplitis an interface with three implementations, not an enum switched over insideExpense.EqualSplit,ExactSplitandPercentSpliteach take different input (nothing, a map of amounts, a map of percentages) and validate it differently -ExactSplitchecks the amounts sum to the total,PercentSplitchecks the percentages sum to 100. Cramming that into one method with a type tag would mean every new split type edits a function that already has two other types' validation logic living in it.Ledgerstores running pairwise balances instead of a list of expenses to replay. Recomputing "what does Alice owe Bob" by walking every expense in the group's history isO(number of expenses)per lookup and gets slower the longer the group has existed. Updating a running balance when an expense is added isO(participants)once, and every later lookup isO(1).- Debt simplification nets each person to a single number before matching anyone up. Cancelling pairwise IOUs directly (Alice owes Bob, Bob owes Charlie) misses the transitive shortcut - Alice could just pay Charlie directly. Reducing everyone to one net number (positive if owed money, negative if owing it) and greedily pairing the largest creditor with the largest debtor is what actually minimizes the number of payments, because it operates on the group's true net position rather than the order expenses happened to be entered in.
- What's missing for a real system: currency rounding (splits have to sum to exactly the total charged, which needs the last participant's share adjusted for any rounding remainder rather than silently drifting by a cent), concurrent expense additions to the same group needing the ledger update to be atomic, and multi-currency groups, which this page's single-currency scope leaves out.
Common follow-ups
- How would you add a fourth split type, splitting by shares rather than percentages?
One new
ExpenseSplitimplementation (sayShareSplit) that converts each participant's share count into a proportional amount -ExpenseandLedgerdo not change at all. - Two expenses get added to the same group at the same time - what's the risk?
Ledger.applySharesreads and writes a shared balances structure with no locking; a real deployment needs that update to be atomic per pair of users, or serialized through a single writer, to avoid losing an update to a race. - How would you support settling up across more than one currency?
ExpenseSplitandLedgerboth currently assume amounts are directly comparable numbers. Multi-currency needs either a conversion to one settlement currency at write time, or aLedgerkeyed by(pair, currency)- a real scope expansion this single-currency design intentionally skips. - A group wants to see "who owes the group the most overall," not just pairwise balances -
is that already answered anywhere? Yes -
simplify()'s intermediate net-position map (positive means owed money) already computes exactly that per person; exposing it directly answers the question without any new logic.
Check yourself
Why is ExpenseSplit an interface with three implementations, rather than an enum switched over inside Expense?