Design Splitwise
Groups, expenses split equally, exactly or by percentage, balances and debt simplification.
- Strategy — Swapping an algorithm at runtime by passing an object.
- Value objects and immutability — Objects defined by their value, never changed, safely shared.
- Graphs inside objects — Dependencies, routes and social links as adjacency behind an API.
- From requirements to classes — Nouns, verbs and the use-case walk that finds the classes.
- Law of Demeter and tell-don't-ask — Talk to your neighbours, not your neighbours' insides.
- Reading and drawing class diagrams — The notation interviewers expect, and what to leave out.
- The machine-coding rubric — What a 90-minute coding-plus-design round is graded on.
01The design, in the six-step framework
1. Requirements and use cases
Splitwise is a machine-coding favourite because the graders can check it with real numbers. Rounding, split validation and debt simplification are where working code separates from a good-looking diagram. Agree on the behaviours first.
- A user creates a group and adds members.
- A member adds an expense: who paid, how much, and how it splits: equally, by exact amounts, or by percentages.
- Anyone views balances: each person's net position, and who owes whom.
- A member settles up by recording a payment to another member.
- The group can simplify debts, reducing the number of payments while leaving each person's net unchanged.
Out of scope: currencies, real payments, notifications, recurring expenses. Constraints: amounts are integer minor units, so sums are exact and never lose a paisa; shares must add up to the expense exactly, with the rounding remainder assigned deterministically; a new split type must not change the group; expenses are immutable, and corrections are new entries, so the history stays auditable.
2. Core entities and relationships
Money: an integer amount in minor units, wrapped as a value with safe arithmetic.SplitStrategy: turns a total and participants into shares:EqualSplit,ExactSplit,PercentSplit.Expense: immutable: id, payer, total, shares.Settlement: a payment of an amount from one member to another.Ledger: the net balance per member, updated from expenses and settlements.Group: the members, the history, and the operations; callers talk only to it.DebtSimplifier: turns net balances into a small list of settlements.
A group composes its ledger and its history. A strategy is used, not stored: it does its work once when the expense is created, and the stored expense keeps the resulting shares. Reading this class diagram aloud is a quick check that nothing else needs to reach into Ledger.
3. Interfaces and key methods
from dataclasses import dataclass
from typing import Dict, List, Protocol
import heapq
class SplitStrategy(Protocol):
def shares(self, total: int, members: List[str]) -> Dict[str, int]:
"""Each member's share in minor units; must sum exactly to total."""
@dataclass(frozen=True)
class Expense:
id: str
payer: str
total: int
shares: tuple # sorted (member, amount) pairs: immutable
@dataclass(frozen=True)
class Settlement:
frm: str
to: str
amount: int
class Group:
def add_expense(self, expense_id: str, payer: str, total: int, members: List[str], strategy: SplitStrategy) -> Expense:
"""Validate, compute shares, update the ledger. A repeated id is a no-op."""
def settle(self, frm: str, to: str, amount: int) -> None:
"""Record a payment; it moves net balances towards zero."""
def balances(self) -> Dict[str, int]:
"""Positive means the member is owed money."""
def suggested_payments(self) -> List[Settlement]:
"""At most n - 1 payments that clear every balance."""
Invariants: the net balances of a group always sum to zero, since every expense and settlement is balanced; an expense's shares sum exactly to its total; expenses never change after creation; simplifying creates suggestions only, and does not alter the ledger until they are recorded as settlements.
4. Design patterns and the choices behind them
Split types as strategies
The force: equal, exact and percent today, by shares or by income tomorrow. Each is a small strategy that returns shares and validates its own input. Group has no if split_type, and a new type touches nothing else. Validation belongs with the strategy, since only ExactSplit knows amounts must sum to the total and only PercentSplit knows percentages must reach 100.
Integer money and the remainder rule
The force: 100 split three ways is 33.33 three times, which loses a paisa with rounding and can never balance with floats. Work in integers: divmod gives a base share and a remainder, and the first few members in sorted order each take one more unit. The sum is then exact, and the assignment is stable, which makes tests possible. Treating an amount as a value and an expense as an immutable record means the history cannot be silently changed.
Net balances, then a graph
Pairwise debts form a directed graph, one of the graphs inside objects, but only each person's net matters for settling up. Reducing the graph to one number per person is the key insight. The greedy then matches the largest creditor with the largest debtor, settling at least one person per payment, so n people need at most n − 1 payments. Note the limit honestly: the true minimum is NP-hard, because it requires finding subsets that sum to zero. For small groups an exact search works; the greedy is the standard answer.
Tell, don't ask, and what graders look for
Group calls ledger.apply_expense(expense) and never does arithmetic on a ledger's dictionary. That is the law of Demeter, and it keeps the zero-sum invariant inside one class. The use cases supply the classes. The machine-coding rubric rewards exactly this: a working run with real numbers, and a new split type that adds a class without editing old ones.
5. Core flow and code
add_expensechecks the id is new, and that the payer and all members are in the group.- The strategy computes shares, and the group verifies they sum to the total.
- An immutable
Expenseis stored, andLedger.apply_expensecredits the payer and debits each share. suggested_paymentspasses the net balances toDebtSimplifier, which pairs creditors with debtors using two heaps.- Each settlement recorded with
settlemoves the two balances towards zero.
import threading
class EqualSplit:
def shares(self, total: int, members: List[str]) -> Dict[str, int]:
base, extra = divmod(total, len(members))
ordered = sorted(members)
return {m: base + (1 if i < extra else 0) for i, m in enumerate(ordered)}
class ExactSplit:
def __init__(self, amounts: Dict[str, int]) -> None:
self.amounts = dict(amounts)
def shares(self, total: int, members: List[str]) -> Dict[str, int]:
if set(self.amounts) != set(members) or sum(self.amounts.values()) != total:
raise ValueError("exact amounts must cover every member and sum to the total")
return dict(self.amounts)
class PercentSplit:
def __init__(self, percents: Dict[str, int]) -> None:
self.percents = dict(percents)
def shares(self, total: int, members: List[str]) -> Dict[str, int]:
if set(self.percents) != set(members) or sum(self.percents.values()) != 100:
raise ValueError("percentages must cover every member and sum to 100")
ordered = sorted(members)
shares = {m: total * self.percents[m] // 100 for m in ordered}
leftover = total - sum(shares.values())
for m in ordered[:leftover]:
shares[m] += 1
return shares
class Ledger:
def __init__(self) -> None:
self.net: Dict[str, int] = {}
def _add(self, member: str, amount: int) -> None:
self.net[member] = self.net.get(member, 0) + amount
def apply_expense(self, e: Expense) -> None:
self._add(e.payer, e.total)
for member, share in e.shares:
self._add(member, -share)
def apply_settlement(self, s: Settlement) -> None:
self._add(s.frm, s.amount)
self._add(s.to, -s.amount)
class DebtSimplifier:
def simplify(self, net: Dict[str, int]) -> List[Settlement]:
creditors = [(-amt, m) for m, amt in net.items() if amt > 0]
debtors = [(amt, m) for m, amt in net.items() if amt < 0]
heapq.heapify(creditors)
heapq.heapify(debtors)
payments = []
while creditors and debtors:
credit, to = heapq.heappop(creditors)
debt, frm = heapq.heappop(debtors)
amount = min(-credit, -debt)
payments.append(Settlement(frm, to, amount))
if -credit > amount:
heapq.heappush(creditors, (credit + amount, to))
if -debt > amount:
heapq.heappush(debtors, (debt + amount, frm))
return payments
class Group:
def __init__(self, name: str, members: List[str]) -> None:
self.name = name
self.members = set(members)
self.ledger = Ledger()
self.history: list = []
self._expense_ids: set = set()
self._lock = threading.Lock()
def add_member(self, user: str) -> None:
with self._lock:
self.members.add(user)
def add_expense(self, expense_id: str, payer: str, total: int, members: List[str], strategy: SplitStrategy) -> Expense:
if total <= 0:
raise ValueError("an expense must be positive")
with self._lock:
existing = next((e for e in self.history if isinstance(e, Expense) and e.id == expense_id), None)
if existing is not None:
return existing # idempotent: a retried request is harmless
unknown = (set(members) | {payer}) - self.members
if unknown:
raise ValueError(f"not in the group: {sorted(unknown)}")
shares = strategy.shares(total, members)
if sum(shares.values()) != total:
raise ValueError("shares do not sum to the total")
expense = Expense(expense_id, payer, total, tuple(sorted(shares.items())))
self.ledger.apply_expense(expense)
self.history.append(expense)
return expense
def settle(self, frm: str, to: str, amount: int) -> None:
with self._lock:
settlement = Settlement(frm, to, amount)
self.ledger.apply_settlement(settlement)
self.history.append(settlement)
def balances(self) -> Dict[str, int]:
with self._lock:
return {m: v for m, v in self.ledger.net.items() if v}
def suggested_payments(self) -> List[Settlement]:
return DebtSimplifier().simplify(self.balances())
if __name__ == "__main__":
g = Group("trip", ["ann", "bo", "cy"])
g.add_expense("e1", "ann", 100, ["ann", "bo", "cy"], EqualSplit())
assert g.balances() == {"ann": 66, "bo": -33, "cy": -33} # ann's share took the extra unit
g.add_expense("e1", "ann", 100, ["ann", "bo", "cy"], EqualSplit())
assert g.balances()["ann"] == 66 # retry changes nothing
g.add_expense("e2", "bo", 90, ["ann", "bo"], PercentSplit({"ann": 50, "bo": 50}))
assert sum(g.balances().values()) == 0
for p in g.suggested_payments():
g.settle(p.frm, p.to, p.amount)
assert g.balances() == {}
The last two lines run the full cycle with real numbers, and a grader will do the same. Reporting the net balance at each step during the interview is the quickest proof that the rounding is right.
6. Extensions, concurrency and what interviewers probe
- "Add a split by shares (2:1:1)." One new
SplitStrategy; it reuses the remainder approach fromPercentSplit. - "Show who owes whom, not just nets." Keep a pairwise map as well, or derive it by replaying the history; the immutable records make replay safe.
- "Edit or delete an expense." Never mutate: append a reversing expense, so balances and the audit trail both remain correct.
- "Is the greedy minimal?" No. With nets of +1, +4, +5, −3, −3 and −4, the greedy uses five payments, but four are enough: +4 and −4 cancel on their own, and the other four people clear in three. Say the exact version is exponential and fine for a group of ten.
- Concurrency: one lock per group, covering the ledger and history together; the expense id makes a retried request idempotent. A real system would index ids rather than scan the history.
- Common mistake: floats for money, and dropping the rounding remainder so a group's balances no longer sum to zero.
02Where this question was reported
- DesignGurus · DesignGurus case study
- GitHub · awesome-low-level-design, Hard
- workat.tech · workat.tech machine coding (SDE I/II)
- systemdesign.academy · systemdesign.academy solved
- LLD Canvas · Min-cash-flow simplification
- interviewcrafted.com · InterviewCrafted example 'Expense Sharing'
- thita.ai · thita.ai LLD sheet, foundational
- Instamock · Tagged Google (SDE1, 60 min); modelling money and balances
- GeeksforGeeks · Google internship Round 2 LLD: design Splitwise; ran over an hour; rejected
- vamsilabs.netlify.app · 'Asked at Google, Amazon, Uber, Flipkart; L4-L6; 45 min'; probes split Strategy, rounding, debt simplification, SOLID
- YouTube · Video 'Design Splitwise'; description frames LLD/machine-coding at Google among others