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.