"Design Splitwise" — or any expense-sharing app for a trip or a flat.
Twelve friends split six bills over a weekend trip, unevenly — some bills split equally, one split by exact amounts, one split by percentage because someone didn't eat the biryani. By Sunday night, nobody can mentally track who owes whom. That's the entire product, in one sentence: turn a pile of shared expenses into one clear balance sheet, and settle it in as few payments as possible.
Clarifying questions
- Do expenses belong to a Group, or can two users split something one-on-one too?
- Which split types are needed — equal, exact amount, percentage?
- Do we need to minimise the number of settlement transactions, or is direct pairwise settling fine?
- Single currency, or multi-currency?
Assume: groups, all three split types, and yes — minimise settlements.
Finding the objects
- User — a person in the system.
- Group — a set of users sharing a running balance sheet (a trip, a flat).
- Expense — one event: who paid, total amount, which split rule applies.
- SplitStrategy — how the total is divided among members.
- Balance — the group's running net amount each pair of members owes each other.
Splitting an expense: a Strategy
"Equal, exact, or percentage" is the Strategy signal you can now spot immediately — more than one interchangeable way to divide the same total, likely to grow a fourth option later:
class SplitStrategy:
def compute(self, total, members, extra=None):
# returns {member: amount_owed}
raise NotImplementedError
class EqualSplit(SplitStrategy):
def compute(self, total, members, extra=None):
share = total / len(members)
return {m: share for m in members}
class ExactSplit(SplitStrategy):
def compute(self, total, members, extra=None):
# extra = {member: exact_amount}, must sum to total
assert sum(extra.values()) == total
return dict(extra)
class PercentageSplit(SplitStrategy):
def compute(self, total, members, extra=None):
# extra = {member: percentage}, must sum to 100
assert sum(extra.values()) == 100
return {m: total * pct / 100 for m, pct in extra.items()}Putting it together
class Expense:
def __init__(self, paid_by, total, members, strategy: SplitStrategy, extra=None):
self.paid_by = paid_by
self.total = total
self.shares = strategy.compute(total, members, extra) # {member: amount}
class Balance:
def __init__(self, members):
# net[a][b] = how much 'a' owes 'b'; kept simplified as we go
self.net = {m: {n: 0 for n in members if n != m} for m in members}
def apply_expense(self, expense: Expense):
for member, amount in expense.shares.items():
if member == expense.paid_by:
continue
self._add(member, expense.paid_by, amount)
def _add(self, ower, owed_to, amount):
# net effect after cancelling any existing opposite debt
self.net[ower][owed_to] += amount
self.net[owed_to][ower] -= amount
class Group:
def __init__(self, members):
self.members = members
self.balance = Balance(members)
self.expenses = []
def add_expense(self, paid_by, total, strategy: SplitStrategy, extra=None):
expense = Expense(paid_by, total, self.members, strategy, extra)
self.expenses.append(expense)
self.balance.apply_expense(expense)
return expenseThe balance sheet
Every expense updates the same running Balance — nobody needs to remember six separate bills; they only ever look at one net number per person:
After 3 expenses in a trip of Aisha, Rohan, Priya: Aisha owes Rohan ₹200 Priya owes Aisha ₹150 Rohan owes Priya ₹100
Simplifying settlements — the algorithmic core
Settling all three debts above directly means three payments. But net it out per person first:
def net_balances(balance: Balance, members):
net = {}
for m in members:
net[m] = sum(balance.net[m].values()) * -1 # owed - owes, net
return net # e.g. {"Aisha": 50, "Rohan": 100, "Priya": -150}
def simplify_debts(net):
creditors = sorted([m for m in net if net[m] > 0], key=lambda m: -net[m])
debtors = sorted([m for m in net if net[m] < 0], key=lambda m: net[m])
transactions = []
i = j = 0
while i < len(debtors) and j < len(creditors):
debtor, creditor = debtors[i], creditors[j]
amount = min(-net[debtor], net[creditor])
transactions.append((debtor, creditor, amount))
net[debtor] += amount
net[creditor] -= amount
if net[debtor] == 0: i += 1
if net[creditor] == 0: j += 1
return transactions # far fewer than settling every pairwise debtNetting first, then greedily matching the biggest debtor with the biggest creditor, collapses the whole group down to the minimum number of settlement transactions — often just one or two, instead of one per original expense.
What if the interviewer changes this?
Expense and Balance both need a currency field, and settlement matching needs a conversion step before netting — SplitStrategy itself doesn't change, since splitting a total is independent of what currency that total is in.Group notifies subscribed members via the same shape as the Library and Parking Lot lessons' notification systems.Common mistakes
- Hard-coding equal split only, instead of a swappable SplitStrategy.
- Settling every original expense pairwise instead of netting balances first.
- Letting rounding errors silently break the invariant that all shares sum to the expense total.
- Mixing group membership, expense storage, and balance calculation all into one God class.
Quick recap
| Class / idea | Job |
|---|---|
| SplitStrategy (Equal/Exact/Percentage) | Swappable way to divide one expense's total. |
| Expense | One event: who paid, how much, split into per-member shares. |
| Balance | Running net amount each pair of members owes, updated per expense. |
| Debt simplification | Net each member's balance, then greedily match debtors to creditors. |
One thing to remember
Don't settle every bill — net every member down to one number first, then match the biggest debtor to the biggest creditor until everyone reaches zero.
Practice Zone
Four MCQs, then two reasoning questions.
Why does 'equal split, exact amounts, or percentage split' point straight at the Strategy pattern?
Asked in


What does a Balance (or ledger) class own that Expense shouldn't own directly?
Asked in

Why is 'minimising the number of settlement transactions' a real algorithmic problem, not just bookkeeping?
Asked in


Why might Group and Expense be modeled as separate classes rather than expenses just being a flat list attached to users directly?
Asked in

Think it through, then reveal:
The first version of Expense.split() hard-codes an equal split among all group members. The interviewer says: "Now support splitting by exact amounts per person, and later by percentage." How should the design change?
Asked in


Walk through, at a high level, how you'd simplify a group's debts into the minimum number of settlement transactions, given each member's net balance (positive = owed money, negative = owes money).
Asked in


FAQ
Do percentage or exact splits always need to sum exactly to the total?
Yes, and this is worth validating explicitly (as the `assert` statements above do) — an expense whose shares don't sum to its total silently corrupts the group's balance sheet, which is a much worse bug than rejecting a malformed split upfront.
Is the debt-simplification algorithm guaranteed to find the absolute minimum number of transactions?
The greedy approach shown here is a strong, commonly used approximation and performs very well in practice — the true minimum-transactions problem is more complex to solve exactly. For an LLD interview, describing and implementing the greedy version, while acknowledging it's a well-performing heuristic rather than a provably optimal solution, is a strong and honest answer.
Should Balance be recalculated from scratch or updated incrementally?
Updating incrementally (as `apply_expense` does above) is far cheaper for a live app with frequent new expenses; recalculating from the full expense history is simpler to reason about but wasteful at scale — a trade-off worth naming if asked.
That completes the course — SOLID, five patterns, UML, and six full systems designed from requirements to code. Go back to any lesson whenever a fresh interview problem reminds you of it, and keep practising with the company questions below.


