Guide · System design
Splitwise System Design: LLD for Splits, Balances and Debt Simplification
23 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- Splitwise system design, at the class level, needs four parts: groups of people, expenses, split rules that decide each person's share, and a balance sheet that adds it all up.
- Store money as whole paise (one rupee is 100 paise), never as decimals. Each split rule must hand out every paisa, so the shares always add up to the bill.
- A net balance is what a person paid minus what they owe. Positive means the group owes them; negative means they owe the group. All balances add up to zero.
- Debt simplification turns many small debts into a few payments. A greedy method, where the person who owes most pays the person owed most, needs at most one payment fewer than the number of people with a balance.
- Beginners can stop after the simplification section. Engineers get runnable Python, a case where greedy needs 7 payments but 6 are enough, and why finding the true minimum is NP-hard, meaning no known fast method.
What goes wrong when friends split shared bills by hand?
Splitting bills by hand loses money in rounding and turns into a tangle of small debts nobody can follow. Splitwise system design is the classic interview problem about fixing both, and it is a common low-level design (LLD) question.
Low-level design means the classes, their data and their methods, as opposed to servers and databases. Splitwise is a well-known expense sharing app, and interviewers use its name as shorthand for the whole problem.
Picture a common setup. Four flatmates in Pune share groceries, Wi-Fi, electricity and the odd cab, and they call each other A, B, C and D. The flat and the amounts are made up, but the rounding bug and the tangle are real.
A pays ₹1,000 for groceries for three people. Round each share to ₹333.33, and the three shares add to ₹999.99, so one paisa has quietly vanished. Over a year of bills, those lost paise become arguments.
The second problem is worse. After four bills, six separate debts point in different directions between four people. Nobody wants to make six bank transfers when three would settle everyone.
Which words do you need before designing an expense sharing app?
You need about eight words, and each one maps to a class or a number in the code. Learn them once here; the rest of the guide uses them the same way.
| Word | Plain meaning | In the flat example |
|---|---|---|
| Group | A set of people who share costs | The four flatmates |
| Expense | One bill: who paid, how much, who shares it | Groceries, ₹1,000, paid by A |
| Share | The part of a bill one person owes | B's share of groceries is ₹333.33 |
| Split rule | The method that turns a bill into shares | Equal, exact amounts or percentages |
| Minor unit | The smallest money unit, stored as a whole number | Paise: ₹1,000 is stored as 100000 |
| Net balance | Total paid minus total owed | C is owed ₹1,121.92 in total |
| Creditor and debtor | Someone owed money, and someone who owes money | C is a creditor, A is a debtor |
| Settle-up | A real payment that clears debt | A pays C ₹710.99 |
What is Splitwise system design, and what must the app do?
Splitwise system design is the design of an app that records shared bills, works out who owes whom and suggests the fewest sensible payments to clear it. In an LLD round, you design the classes and the core logic, not the servers.
Start by agreeing on requirements. Interviewers reward you for asking before drawing boxes, and the list below is a sensible scope for a 45-minute round.
- Users can create a group and add members.
- Anyone in the group can add an expense, naming the payer, the total and who shares it.
- The app supports three split rules: equal, exact amounts and percentages.
- Each person can see their net balance and who they owe.
- The app suggests a short list of payments that settles the group.
- A settle-up payment is recorded, so balances drop to zero once everyone pays.
Two rules matter more than any feature. Money must never be lost or created by rounding. And simplifying debts must never change what anyone owes overall.
The real app makes that second promise in public. Its help page on simplify debts says the feature never changes anyone's total balance and only changes the payment paths, as of 25 October 2026. This guide does not claim to know how that app computes its plan internally.
Which classes does an expense sharing app need?
You need five small classes and one function: User, Group, Expense, a SplitRule family and a settle-up planner. Each class owns one job, which makes the design easy to test and easy to change in the interview.
| Class | Owns | Main method |
|---|---|---|
| User | An id and a display name | None; it is plain data |
| Group | Members and the list of expenses | add_expense, net_balances, record_payment |
| Expense | Payer, total in paise, and each person's share | None; it is a record that never changes |
| SplitRule | The method for turning a total into shares | shares(total, members) |
| EqualSplit, ExactSplit, PercentSplit | One split method each | shares, with their own checks |
| simplify function | Turning balances into a payment list | simplify(balances) |
SplitRule uses the Strategy pattern, which means the varying part (how to split) sits behind one shared method. Group calls rule.shares and never asks which rule it has. Adding a fourth rule, such as split by shares like 2:1:1, means adding one class and changing nothing else.
The classes of an expense sharing app
Drag to exploreNotice that Group never does the splitting itself. It asks a SplitRule for shares, stores the result in an Expense, and later sums all expenses into balances.
Diagram connections:
- Group to User (has members)
- Group to Expense (keeps a list of)
- Expense to SplitRule (shares come from)
- EqualSplit to SplitRule (is a)
- ExactSplit to SplitRule (is a)
- PercentSplit to SplitRule (is a)
- Group to Net balances (sums expenses into)
- Net balances to simplify() (feeds)
- simplify() to Payment list (returns)
- User: An id and a name
- Group: Holds members and expenses
- Expense: Payer, total in paise, shares
- SplitRule: One method: shares(total, members)
- EqualSplit: Same share, spare paise spread
- ExactSplit: Given amounts, must add up
- PercentSplit: Basis points, largest remainder
- Net balances: Paid minus owed, per person
- simplify(): Greedy settle-up plan
- Payment list: Who pays whom, how much
How do equal, exact and percentage splits work without losing paise?
Each rule works in whole paise and hands out every leftover paisa to someone, so the shares always add up to the bill. The naive version uses decimals, and it is wrong.
The naive way: decimals and rounding
A first try stores money as a floating-point number, which is a binary fraction, and rounds each share. Python's documentation explains the catch: most decimal fractions, such as 0.1, have no exact binary form. In Python, 0.1 + 0.2 == 0.3 prints False.
Rounding each share then makes it worse. Our run of round(1000 / 3, 2) * 3 printed 999.99, so the bill and the shares disagree by one paisa. This naive method is shown only so you can name it in an interview.
The fix: whole paise and a rule for leftovers
Store every amount as a whole number of paise. India's Coinage Act, 2011 divides the rupee into one hundred units, so ₹1,000 becomes 100000 and ₹1,457.25 becomes 145725. Integers add and subtract exactly.
Splitting still leaves leftovers, because 100000 does not divide by 3. Python's divmod gives the quotient and the remainder together: divmod(100000, 3) is (33333, 1). That 1 is a spare paisa, and someone must take it.
Our rule gives spare paise to people in id order, one each. So A pays ₹333.34 and B and C pay ₹333.33. The three shares add to exactly ₹1,000.00.
| Rule | Input | Flat example | Shares (₹) |
|---|---|---|---|
| Equal | Total and people | Groceries ₹1,000 among A, B, C | 333.34, 333.33, 333.33 |
| Equal | Total and people | Wi-Fi ₹1,179 among all four | 294.75 each, no leftover |
| Exact | An amount per person | Electricity ₹2,450 by room meter | 500, 600, 700, 650 |
| Percentage | Percent per person, in basis points | Cab ₹1,457.25 at 40, 30, 30 percent | 582.90, 437.18, 437.17 |
Percentages use basis points, where one basis point is 0.01 percent and 100 percent is 10000. That keeps a share like 33.33 percent a whole number too. The cab's 30 percent shares are each ₹437.175, which is half a paisa.
The fix is the largest remainder method. Round every share down, count the paise still missing, and give one each to the people who lost the most in rounding. B and D tie, so the tie goes to B by id, and B pays ₹437.18.
Here is part 1 of the code. The three parts form one file, splitwise.py, using only the Python standard library. If you are new to Python, skim the code; the tables already show what it does.
# Part 1: users and the three split rules (Strategy pattern)
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
@dataclass(frozen=True)
class User:
user_id: str
name: str
class SplitRule(ABC):
@abstractmethod
def shares(self, total, members):
"""Return {user_id: paise}. The values must add up to total."""
class EqualSplit(SplitRule):
def shares(self, total, members):
base, extra = divmod(total, len(members))
ids = sorted(m.user_id for m in members)
# The first `extra` people (sorted by id) pay one more paisa each.
return {uid: base + (1 if i in range(extra) else 0)
for i, uid in enumerate(ids)}
class ExactSplit(SplitRule):
def __init__(self, amounts):
self.amounts = amounts # {user_id: paise}
def shares(self, total, members):
if sum(self.amounts.values()) != total:
raise ValueError("exact amounts must add up to the total")
return dict(self.amounts)
class PercentSplit(SplitRule):
def __init__(self, basis_points):
self.bp = basis_points # 1 basis point = 0.01%, so 100% = 10000
def shares(self, total, members):
if sum(self.bp.values()) != 10000:
raise ValueError("percentages must add up to 100")
raw = {uid: total * bp for uid, bp in self.bp.items()}
out = {uid: r // 10000 for uid, r in raw.items()}
left = total - sum(out.values())
# Hand leftover paise to the largest remainders; ties go by user id.
order = sorted(raw, key=lambda u: (-(raw[u] % 10000), u))
for uid in order[:left]:
out[uid] += 1
return outWe checked the promise rather than trusting it. A test ran 20,000 random equal and percentage splits on totals up to ₹1 lakh (100,000 rupees). Every split added up exactly, and equal shares never differed by more than one paisa.
How do you compute who owes whom from the expenses?
Add up each person's net balance: everything they paid, minus every share they owe. Positive means the group owes them; negative means they owe the group. Because every bill's shares add up to its total, all balances together always add up to zero.
That zero sum is your best test. If balances ever add to anything else, a paisa leaked somewhere. Our code refuses to plan payments when the sum is not zero.
Part 2 holds the expense record, the group and the balance maths.
# Part 2: expenses, groups and balances
@dataclass
class Expense:
expense_id: str
paid_by: str
total: int # paise
shares: dict # {user_id: paise}
note: str = ""
@dataclass
class Group:
group_id: str
members: dict = field(default_factory=dict) # {user_id: User}
expenses: list = field(default_factory=list)
def add_member(self, user):
self.members[user.user_id] = user
def add_expense(self, expense_id, paid_by, total, rule, among, note=""):
if max(total, 0) == 0:
raise ValueError("total must be a positive number of paise")
if paid_by not in self.members:
raise ValueError("payer is not in this group")
people = [self.members[u] for u in among]
shares = rule.shares(total, people)
if not set(shares).issubset(among) or sum(shares.values()) != total:
raise ValueError("shares must belong to the chosen people and add up")
expense = Expense(expense_id, paid_by, total, shares, note)
self.expenses.append(expense)
return expense
def record_payment(self, payment_id, payer, receiver, amount):
# A settle-up is an expense paid by the payer and owed by the receiver.
return self.add_expense(payment_id, payer, amount,
ExactSplit({receiver: amount}), [receiver],
"payment")
def net_balances(self):
"""Positive: the group owes you. Negative: you owe the group."""
bal = {uid: 0 for uid in self.members}
for e in self.expenses:
bal[e.paid_by] += e.total
for uid, share in e.shares.items():
bal[uid] -= share
return bal
def pairwise_debts(self):
"""Raw who-owes-whom before simplification, A-to-B netted against B-to-A."""
owe = {}
for e in self.expenses:
for uid, share in e.shares.items():
if uid != e.paid_by and share:
key = (uid, e.paid_by)
owe[key] = owe.get(key, 0) + share
net = {}
for (a, b), amt in owe.items():
left = max(amt - owe.get((b, a), 0), 0)
if left:
net[(a, b)] = left
return netNotice that our code never changes an Expense after storing it. Balances are computed from the full list of expenses each time. That makes edits and deletes simple: change the list, and the balances follow.
A settle-up payment is stored as an expense too. If B pays D ₹75.33, it is recorded as an expense of ₹75.33 paid by B and owed fully by D. B's balance rises and D's falls by the same amount.
The raw debts in the flat
If you list who owes each payer for each bill, and cancel A-to-B against B-to-A, the flat ends up with six separate debts. That is the tangle from the opening.
Six raw debts after four bills
Drag to exploreEach arrow points from the person who owes to the person who paid. Notice how money passes through middle people: B owes A, while A owes D and D owes C. Simplification cuts out the middle steps.
Diagram connections:
- Flatmate A to Flatmate C (₹166.67)
- Flatmate A to Flatmate D (₹582.90)
- Flatmate B to Flatmate A (₹38.58)
- Flatmate B to Flatmate C (₹305.25)
- Flatmate B to Flatmate D (₹142.43)
- Flatmate D to Flatmate C (₹650.00)
- Flatmate A: Net: owes ₹710.99
- Flatmate B: Net: owes ₹486.26
- Flatmate C: Net: is owed ₹1,121.92
- Flatmate D: Net: is owed ₹75.33
Now read the net balances in the diagram's boxes. They are the only numbers that matter for settling up. Who paid which bill no longer matters once you know each person's total.
How does debt simplification settle a group in fewer payments?
Debt simplification ignores the tangle and looks only at net balances. The people who owe pay the people who are owed, until everyone is at zero. Totals stay the same; only the paths change.
The real app's help page gives the simplest case, as of 25 October 2026. If one person owes a second ₹20 and the second owes a third ₹20, the first pays the third directly and the middle person drops out.
An everyday picture: the common pot
Think of a steel dabba on the kitchen shelf as a common pot. Everyone who owes drops their net amount in. Everyone who is owed takes their net amount out. The pot ends empty because balances add to zero.
- Dropping money into the pot maps to a negative net balance being paid off.
- Taking money out maps to a positive net balance being received.
- The pot ending empty maps to all balances adding up to zero.
Where the picture breaks: there is no real pot. Each payment must go from one person straight to another. Deciding who pays whom, in as few payments as possible, is the hard part the pot hides.
The greedy method, step by step
The usual interview answer is greedy, which means making the best-looking choice at each step without looking ahead. The person who owes the most pays the person owed the most, as much as one of them needs.
The code keeps two heaps. A heap is a structure that always hands back its smallest item quickly. Python's heapq module is a min-heap, so creditors are stored as negative amounts to come out largest first. Debtors' balances are already negative.
Greedy settle-up loop
Drag to exploreEvery pass of the loop clears at least one person to zero. That is why the loop ends, and why it needs at most one payment fewer than the number of people with a balance.
Diagram connections:
- Net balances to Make two heaps (split by sign)
- Make two heaps to Take the largest of each (start)
- Take the largest of each to Pay the smaller amount (match the pair)
- Pay the smaller amount to Anyone still unsettled? (check)
- Anyone still unsettled? to Put any leftover back (Yes)
- Put any leftover back to Take the largest of each (next pass)
- Anyone still unsettled? to Payment plan (No)
- Net balances: Must add up to zero
- Make two heaps: Owed people and owing people
- Take the largest of each: Most owed and most owing
- Pay the smaller amount: One transfer added to the plan
- Anyone still unsettled?: Check both heaps
- Put any leftover back: Whoever is not yet at zero
- Payment plan: List of who pays whom
Part 3 is the whole planner.
# Part 3: greedy settle-up (largest debtor pays largest creditor)
import heapq
def simplify(balances):
if sum(balances.values()) != 0:
raise ValueError("balances must add up to zero")
creditors = [(-amt, uid) for uid, amt in balances.items() if max(amt, 0)]
debtors = [(amt, uid) for uid, amt in balances.items() if min(amt, 0)]
heapq.heapify(creditors) # most-owed person comes out first
heapq.heapify(debtors) # person who owes most comes out first
transfers = []
while creditors and debtors:
c_amt, c = heapq.heappop(creditors)
d_amt, d = heapq.heappop(debtors)
pay = min(-c_amt, -d_amt)
transfers.append((d, c, pay))
if -c_amt != pay:
heapq.heappush(creditors, (c_amt + pay, c))
if -d_amt != pay:
heapq.heappush(debtors, (d_amt + pay, d))
return transfers
def rupees(paise):
sign = "-" if min(paise, 0) else ""
r, p = divmod(abs(paise), 100)
return f"{sign}Rs {r}.{p:02d}"Run it on the flat. This short script adds the four bills, prints the balances and the plan, then records the payments.
from splitwise import Group, User, EqualSplit, ExactSplit, PercentSplit, simplify, rupees
flat = Group("flat")
for uid in "ABCD":
flat.add_member(User(uid, "Flatmate " + uid))
flat.add_expense("e1", "A", 100000, EqualSplit(), list("ABC"), "groceries")
flat.add_expense("e2", "B", 117900, EqualSplit(), list("ABCD"), "wifi")
flat.add_expense("e3", "C", 245000,
ExactSplit({"A": 50000, "B": 60000, "C": 70000, "D": 65000}),
list("ABCD"), "electricity")
flat.add_expense("e4", "D", 145725,
PercentSplit({"A": 4000, "B": 3000, "D": 3000}),
list("ABD"), "cab")
balances = flat.net_balances()
print({uid: rupees(p) for uid, p in balances.items()}, "sum:", sum(balances.values()))
print("raw debts:", len(flat.pairwise_debts()))
plan = simplify(balances)
for payer, receiver, paise in plan:
print(payer, "pays", receiver, rupees(paise))
for i, (payer, receiver, paise) in enumerate(plan):
flat.record_payment(f"p{i}", payer, receiver, paise)
print("after settle-up:", flat.net_balances())The output on Python 3.13.17 was this. The same script printed identical output on Python 3.11 and 3.12.
{'A': '-Rs 710.99', 'B': '-Rs 486.26', 'C': 'Rs 1121.92', 'D': 'Rs 75.33'} sum: 0
raw debts: 6
A pays C Rs 710.99
B pays C Rs 410.93
B pays D Rs 75.33
after settle-up: {'A': 0, 'B': 0, 'C': 0, 'D': 0}| Raw debts | Greedy plan | |
|---|---|---|
| Number of payments | 6 | 3 |
| Payments | A to C, A to D, B to A, B to C, B to D, D to C | A pays C ₹710.99, B pays C ₹410.93, B pays D ₹75.33 |
| Each person's total | A owes ₹710.99, B owes ₹486.26 | Unchanged |
| Check after paying | Not applicable | All four balances are 0 |
Notice that A's cab debt to D has disappeared. A pays only C, and D is paid by B instead. That is the trade-off of simplifying: fewer payments, but money may not go to the person you shared that bill with.
Is greedy debt simplification always the fewest payments?
No. Greedy is fast and never needs more than n minus 1 payments, where n is the number of people with a non-zero balance. But it sometimes uses more payments than the true minimum.
Why greedy needs at most n minus 1 payments
Each greedy payment brings at least one person to zero, and the last payment clears two. A 2004 paper by Tom Verhoeff gives the same N minus 1 bound for a greedy settlement, using a graph argument.
Our test checked it on 3,000 random groups of 2 to 9 people. Greedy settled every group and never broke the n minus 1 bound.
A case where greedy needs one extra payment
Take a trip group of eight. P and Q are each owed ₹100. R, S, T, U, V and W owe ₹30, ₹33, ₹37, ₹26, ₹34 and ₹40.
Greedy matches the biggest numbers first: W pays P ₹40, T pays Q ₹37, and so on. R's ₹30 ends up split between Q and P, which costs a seventh payment.
The best plan needs six. R, S and T pay P exactly ₹100 between them, and U, V and W pay Q ₹100. Each person pays once.
What the true minimum depends on
The minimum is n minus the largest number of separate groups whose balances each add to zero. In the trip, {P, R, S, T} and {Q, U, V, W} each add to zero, so 8 people minus 2 groups gives 6 payments.
Verhoeff's paper states this partition view, and later papers by Csaba Pătcaș use the same formula. Every extra zero-sum group you find saves exactly one payment.
For small groups, you can find the minimum exactly with bitmask dynamic programming. Dynamic programming means solving small sub-problems once and reusing their answers. A bitmask is a whole number whose bits mark which people are in a subset. The method tries every subset once and remembers the best answer for each.
# Part 4: the exact minimum for small groups (bitmask dynamic programming)
def fewest_transfers(balances):
vals = [v for v in balances.values() if v != 0]
n = len(vals)
full = 2 ** n - 1
total = [0] * (full + 1) # total[mask] = sum of balances in mask
groups = [0] * (full + 1) # most zero-sum groups that fit inside mask
for mask in range(1, full + 1):
low = mask & -mask
total[mask] = total[mask ^ low] + vals[low.bit_length() - 1]
best = 0
rest = mask
while rest:
bit = rest & -rest
best = max(best, groups[mask ^ bit])
rest ^= bit
groups[mask] = best + (1 if total[mask] == 0 else 0)
return n - groups[full]Add this as part 4. On the trip it returns 6, while greedy returns 7. We checked it against a brute-force search of every way to group people, on 400 random small groups, and the answers matched every time.
Why the exact minimum is NP-hard
NP-hard means no known method finds the answer in time that grows only polynomially with the input. A fast method would give fast methods for every problem in the class NP. Verhoeff (2004) shows that finding the fewest payments is at least as hard as 3-Partition.
3-Partition asks whether a list of numbers can be split into triples that all have the same sum. It is NP-complete in the strong sense: it stays hard even when the numbers are written in unary, as rows of tally marks. So tricks that are fast for small numbers do not help. A 2014 paper by Pătcaș and Bartha states that the debts problem is NP-hard in the strong sense.
You can feel this in the timings. On our test machine, the exact method took 0.05 seconds for 15 people, 1.61 seconds for 20, 3.33 for 21 and 7.41 for 22. Each extra person roughly doubles the time.
Greedy took under one second for 100,000 people on the same machine. With a heap, it runs in roughly n log n steps, where log n grows very slowly as n grows.
| Greedy (largest pays largest) | Exact (bitmask search) | |
|---|---|---|
| Payments | At most n minus 1; sometimes one or more above the minimum | The true minimum |
| Time on our machine | Under 1 second for 100,000 people | 1.61 seconds for 20 people, doubling per person |
| How often worse | Above the minimum in 288 of 3,000 random small groups in our test | Never |
| Good for | Any group size | Small groups, roughly 20 people or fewer |
The 288 in 3,000 figure comes from our own random test, with balances between minus 50 and plus 50. Real groups may behave differently, so treat it as a sign that the gap exists, not as a rate.
How do you test an expense sharing app's money logic?
Test invariants, which are facts that must stay true whatever the input. Money code is easier to test this way than with a few hand-picked examples, because random inputs find the rounding edges for you.
- Every expense's shares add up exactly to its total.
- Equal shares differ by at most one paisa.
- Every percentage share is within one paisa of its exact value.
- All net balances add up to zero.
- After recording the greedy plan as payments, every balance is zero.
- The plan never has more than n minus 1 payments, and never fewer than the exact minimum.
Also test the refusals. Our code raises an error for six bad inputs. They are a payer outside the group, a share for someone not on the bill and a zero total. The others are exact amounts that do not add up, percentages short of 100 and balances that do not sum to zero.
To check your own build, run the demo script and compare it with the output above. The balances must read minus ₹710.99, minus ₹486.26, plus ₹1,121.92 and plus ₹75.33, and the last line must show four zeros.
What changes when an expense sharing app runs in production?
The classes stay, but storage, edits, retries and currencies need decisions. These are design choices to discuss, not facts about any one app.
Expenses are the source of truth
Store expenses and payments as records, and treat balances as derived numbers. You may cache a balance per person for speed, but you should be able to rebuild it from the records at any time. Rebuilding is also how you find a bug.
An edit should replace the old expense with a new version and keep the history. Silent edits in place make arguments impossible to settle later.
Retries must not add an expense twice
Phones lose signal, and an app may send the same add-expense request twice. Give each request an idempotency key, a unique id the server remembers, so a repeat is ignored. The SevyDevy guide on payment system design covers this idea in depth.
Simplified plans are suggestions, payments are history
Do not store the simplified plan as debt. Recompute it from balances whenever someone opens the group, because any new expense changes it. Only the payments people actually make are stored.
The real app's help page shows why this matters, as of 25 October 2026. It recommends keeping simplify debts switched on once payments have been made, because turning it off can leave people needing to resend money. Payments that followed one plan look odd under another.
Currencies and groups
Keep a separate balance for each currency. Never add rupees to dollars inside the balance sheet; convert only when a user asks to settle in one currency, and store the rate used.
Simplify within one group at a time unless your product says otherwise. Mixing groups can make a flatmate pay a trip friend they have never met, which may surprise people.
How do you explain Splitwise system design in a system design interview?
Lead with requirements, then classes, then the money rules, then simplification and its limits. This order shows judgement before cleverness.
Confirm the scope
Ask about groups, the split rules needed, whether settle-ups are recorded and whether multiple currencies matter. Write the agreed list down.
State the money rule
Say you will store amounts as integer paise and that every split hands out every paisa. Show the ₹1,000 among three example.
Draw the classes
User, Group, Expense, SplitRule with three strategies, and a simplify function. Explain why SplitRule is a Strategy.
Define net balance
Paid minus owed, positive means owed money, and the sum of all balances is always zero. Offer that as your main test.
Explain greedy simplification
Largest debtor pays largest creditor using two heaps. It needs at most n minus 1 payments and runs in about n log n time.
Name the limit
Say greedy is not always minimal, the exact minimum is NP-hard, and an exact search works only for small groups.
Cover production
Expenses as the source of truth, versioned edits, idempotency keys, per-currency balances and plans recomputed on read.
Questions people ask
- What is Splitwise system design in an interview?
- It is a design question about an app that records shared bills and settles them. In an LLD round you design classes for users, groups, expenses and split rules, compute net balances, and suggest a short list of payments. Good answers store money in paise and explain where greedy simplification falls short of the true minimum.
- Why should money be stored in paise and not as decimals?
- Because whole numbers add exactly and binary decimals do not. In Python, 0.1 + 0.2 == 0.3 is False, and rounding ₹1,000 split three ways to ₹333.33 each loses a paisa. Storing 100000 paise and handing the spare paisa to one person keeps every bill exact.
- How does the debt simplification algorithm work?
- It works from net balances, not individual bills. The greedy version repeatedly matches the person who owes the most with the person owed the most and pays the smaller of the two amounts. Each payment clears at least one person, so it needs at most one payment fewer than the people with a balance.
- Does simplifying debts change how much anyone owes?
- No. Simplification only changes who pays whom, not anyone's total. The real app's help page says its simplify debts feature never changes anyone's total balance, as of 25 October 2026. In code, you can check this: the net balances before and after recording the plan must match the original totals.
- Is minimising the number of transactions NP-hard?
- Yes, finding the fewest payments is NP-hard. A 2004 paper by Tom Verhoeff shows it is at least as hard as 3-Partition, and a 2014 paper states it is NP-hard in the strong sense. That is why interview answers usually use a greedy method, with an exact search only for small groups.
- Which design pattern fits the split rules?
- The Strategy pattern fits best. Each split rule, equal, exact or percentage, is a class with the same shares method, and the group calls that method without knowing which rule it holds. Adding a new rule, such as splitting by shares like 2:1:1, means adding one class without touching Group.
- Is the greedy settle-up the same as the minimum cash flow algorithm?
- Both names describe the same idea: people who owe pay people who are owed, largest amounts first. The method moves no more money than the debtors owe in total, which is the least possible. But it does not always give the fewest payments. In our trip example it used 7 payments where 6 were enough.
Sources and further reading
- Splitwise help centre: What is Simplify Debts? (opened 25 October 2026)
- Tom Verhoeff, Settling Multiple Debts Efficiently: An Invitation to Computing Science, Informatics in Education 3(1), 2004
- Csaba Pătcaș and Attila Bartha, Evolutionary solving of the debts' clearing problem (arXiv, 2014)
- Csaba Pătcaș, The debts' clearing problem: a new approach (arXiv, 2011)
- Python documentation: Floating-Point Arithmetic, Issues and Limitations
- Python documentation: heapq, heap queue algorithm
- The Coinage Act, 2011, section 7 (India Code PDF)