Step 1 Understand it
Why interviewers ask this. It tests clean modelling of money, validation, and a small algorithm (debt simplification), and it catches people who use floats for money.
Watch it happen
Read the animation as text
- Keep ONE net balance per person: positive means they are owed money, negative means they owe. Everyone starts at zero.
- Ana pays £80 for dinner for all four. Split equally, each share is £20: Ana is owed £60, everyone else owes £20.
- Ben pays £30 for a taxi for himself, Cai and Dev: £10 each. Balances add up — and always sum to zero.
- Simplify: match the biggest debtor with the biggest creditor. Dev pays Ana £30.
- Repeat: Cai pays Ana £30.
- Everyone is at zero after 2 payments, instead of a payment for every expense share.
Requirements
Functional
- Users and groups.
- Add an expense paid by one person, split equally, by exact amounts, or by percentage.
- Show balances: who owes whom, and how much.
- Settle up, and simplify the debts in a group.
Non-functional
- Money must be exact: no floating-point errors.
- Splits must always add up to the total.
Classes
- User; Group (members, expenses).
- Expense: payer, amount, splits, description.
- Split (abstract) → EqualSplit, ExactSplit, PercentSplit.
- SplitFactory: creates and validates splits.
- BalanceSheet: the net balance per user.
- DebtSimplifier: reduces the number of payments.
Patterns used
- or (split types)
- (tell members about new expenses)
Start simple, then scale
- Version 1: equal splits only, stored as pairwise 'A owes B' records.
- Version 2: exact and percentage splits, behind a split strategy that validates them.
- Version 3: one net balance per user, plus debt simplification.
- Version 4: groups, several currencies, history and notifications.
Picture it
classDiagram Group "1" o-- "many" User Group "1" *-- "many" Expense Expense "1" *-- "many" Split Expense --> User : paid by Split <|-- EqualSplit Split <|-- ExactSplit Split <|-- PercentSplit BalanceSheet --> User : net balance per user DebtSimplifier --> BalanceSheet
The design, step by step
- Add an expense: validate the split (it sums to the total) → each participant now owes the payer their share.
- Keep one net balance per user: positive means they're owed money, negative means they owe.
- Simplify debts: repeatedly match the biggest debtor with the biggest creditor until everyone is at zero.
Deep dives
- Store money as whole pence (or Decimal), never as float.
- Rounding: £10 split three ways is 334 + 333 + 333 pence; give the extra penny to someone in a fixed, fair way.
- The simplification settles n people in at most n − 1 payments.
Common mistakes
- Floats for money.
- Splits that don't add up to the total.
- Losing the leftover pence when rounding.
from collections import defaultdict
import heapq
net = defaultdict(int) # in pence: + is owed money, - owes money
def add_equal_expense(payer, amount_pence, people):
share, remainder = divmod(amount_pence, len(people))
for i, person in enumerate(people):
owed = share + (1 if i < remainder else 0) # spread the leftover pence
net[person] -= owed
net[payer] += owed
def simplify():
creditors = [(-v, p) for p, v in net.items() if v > 0]
debtors = [(v, p) for p, v in net.items() if v < 0]
heapq.heapify(creditors)
heapq.heapify(debtors)
payments = []
while creditors and debtors:
c_amt, c = heapq.heappop(creditors)
d_amt, d = heapq.heappop(debtors)
pay = min(-c_amt, -d_amt)
payments.append((d, c, pay)) # d pays c this many pence
if -c_amt > pay:
heapq.heappush(creditors, (c_amt + pay, c))
if -d_amt > pay:
heapq.heappush(debtors, (d_amt + pay, d))
return paymentsStep 2 Go further (free, optional)
Optional. The notes above are written to be enough on their own. These free resources give a second explanation and extra practice.
-
Watch
YouTube
-
Code
Awesome Low-Level Design (questions and solutions)
GitHub, free
Step 3 Practise
Try each exercise yourself first: on paper, out loud or in code. Then open the answer and compare it with yours.
-
ExplainSay it in plain words in under a minute, with no notes.
Explain how balances and debt simplification work, with an example of 3 friends.
Show answer
Three friends: Asha, Ben and Chen. Each person has one net balance, in pence: positive means they are owed money, negative means they owe.
1. Asha pays £30 for dinner, split equally: each share is £10. Ben and Chen each owe Asha £10, and Asha's own share cancels out. Balances: Asha +£20, Ben −£10, Chen −£10.
2. Ben pays £12 for a taxi, split equally: £4 each. Balances: Asha +£16, Ben −£2, Chen −£14. (They always add up to zero.)Simplifying: instead of paying back each expense separately (Ben → Asha £10, Chen → Asha £10, Asha → Ben £4, Chen → Ben £4: four payments), match the biggest debtor with the biggest creditor, again and again:
• Chen (−£14) pays Asha (+£16) £14. Asha is now +£2, Chen is settled.
• Ben (−£2) pays Asha (+£2) £2. Everyone is at zero.
Two payments settle everything. The greedy method always needs at most n − 1 payments for n people. -
DrawSketch the diagram (or class diagram) from memory.
Draw the , including the Split .
Show answer
An Expense owns its splits; the abstract Split has one subclass per way of splitting. SplitFactory builds the right splits and rejects any that don't add up to the total. Every expense updates the BalanceSheet, which DebtSimplifier reads to suggest the fewest payments.
textGroup 1 ◆── * Expense - members: User[*] - payer: User - amount_pence - description - splits ◆── 1..* Split «abstract» - user: User - amount_pence △ ┌─────────────────┼─────────────────┐ EqualSplit ExactSplit PercentSplit (given amount) (given percent) SplitFactory + create(kind, total, people, values) → Split[] (checks the sum) BalanceSheet - net: User → pence ◄── updated by each new Expense DebtSimplifier + simplify(balance_sheet) → [(from, to, pence)] -
ApplyUse it in a real design question and say exactly where it fits.
Add percentage and exact splits with validation, and test that £10 split three ways gives 334, 333 and 333 pence.
Show answer
Money is whole pence (integers) throughout. Each split type returns who owes what, and refuses splits that don't add up: exact amounts must sum to the total, and percentages must sum to 100. Integer division leaves a few pence over, which are handed out one each, to people in a fixed order, so the shares always add up to the total exactly.
£10 split three ways gives 334, 333 and 333 pence. £9.99 split 50/25/25 gives 500, 250 and 249: the 2 leftover pence go to the first two people.
pythonfrom collections import defaultdict def equal_split(total, people): share, remainder = divmod(total, len(people)) return {p: share + (1 if i < remainder else 0) for i, p in enumerate(people)} def exact_split(total, amounts): if sum(amounts.values()) != total: raise ValueError(f"exact amounts add up to {sum(amounts.values())}, not {total}") return dict(amounts) def percent_split(total, percents): if sum(percents.values()) != 100: raise ValueError(f"percentages add up to {sum(percents.values())}, not 100") shares = {p: total * pct // 100 for p, pct in percents.items()} leftover = total - sum(shares.values()) for p in list(percents)[:leftover]: # the leftover pence, one each, in a fixed order shares[p] += 1 return shares net = defaultdict(int) # pence: + is owed money, - owes money def add_expense(payer, shares): for person, owed in shares.items(): net[person] -= owed net[payer] += owed shares = equal_split(1000, ["asha", "ben", "chen"]) assert sorted(shares.values()) == [333, 333, 334] and sum(shares.values()) == 1000 print(shares) # {'asha': 334, 'ben': 333, 'chen': 333} print(percent_split(999, {"asha": 50, "ben": 25, "chen": 25})) # {'asha': 500, 'ben': 250, 'chen': 249} for bad in (lambda: exact_split(1000, {"asha": 500, "ben": 400}), lambda: percent_split(1000, {"asha": 60, "ben": 30})): try: bad() except ValueError as error: print(error) # exact amounts add up to 900, not 1000 # percentages add up to 90, not 100 add_expense("asha", equal_split(3000, ["asha", "ben", "chen"])) # dinner, £30 add_expense("ben", equal_split(1200, ["asha", "ben", "chen"])) # taxi, £12 print(dict(net)) # {'asha': 1600, 'ben': -200, 'chen': -1400} -
Trade-offsSay what it costs, when not to use it, and the alternative.
Store every pairwise balance, or just one net balance per user? What does each make easy or hard?
Show answer
Pairwise balances (for every pair of people, how much one owes the other):
• Easy: showing "you owe Ben £6" directly, per friend and per group; settling up with one particular person; explaining where a number came from.
• Hard: storage grows with the square of the number of people; one expense updates many pairs; and paying everything back pair by pair needs many more payments than necessary.One net balance per user:
• Easy: tiny storage (one number per person); an expense is a few additions; "am I owed or do I owe, overall?" is instant; simplifying to at most n − 1 payments falls straight out of it.
• Hard: you lose who owes whom. The simplified plan might ask you to pay someone you never shared a bill with, which confuses users, and you can't show a per-friend balance without going back to the expenses.In practice, keep the expenses as the source of truth (they can always be replayed), keep net balances per group for speed, and work out pairwise views or simplified payments when someone asks to see or settle them.
Step 4 Interview-ready, then prove it
Your interview answer
About 20 to 40 seconds, said out loud. Practise it until it sounds like you, not like notes.
Expenses have a payer and a list of splits, created by a split strategy that checks they add up to the total. Each expense updates a net balance per user, stored in whole pence. To settle a group, I greedily match the largest debtor with the largest creditor, which needs at most n − 1 payments.
Follow-up questions
Why not use floats for money?
0.1 + 0.2 isn't exactly 0.3 in binary floating point. Money needs whole minor units (pence) or Decimal.
Is the greedy method optimal?
It always finishes in at most n − 1 payments, which is good. Finding the true minimum is NP-hard in general, so greedy is the practical choice.
Tick each check when you can do it without notes
A topic is finished when you pass all four checks, not when you finish a video. Be strict: if you needed your notes, it is not ticked yet.
- Explain Say it in plain words in under a minute, with no notes.
- Draw Sketch the diagram (or class diagram) from memory.
- Apply Use it in a real design question and say exactly where it fits.
- Trade-offs Say what it costs, when not to use it, and the alternative.
0 of 4 checks ticked.
Saved in this browser. Back it up or move it to another device.