SevyDevy

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.

The vocabulary, mapped to the running example
WordPlain meaningIn the flat example
GroupA set of people who share costsThe four flatmates
ExpenseOne bill: who paid, how much, who shares itGroceries, ₹1,000, paid by A
ShareThe part of a bill one person owesB's share of groceries is ₹333.33
Split ruleThe method that turns a bill into sharesEqual, exact amounts or percentages
Minor unitThe smallest money unit, stored as a whole numberPaise: ₹1,000 is stored as 100000
Net balanceTotal paid minus total owedC is owed ₹1,121.92 in total
Creditor and debtorSomeone owed money, and someone who owes moneyC is a creditor, A is a debtor
Settle-upA real payment that clears debtA 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.

Classes and what each one owns
ClassOwnsMain method
UserAn id and a display nameNone; it is plain data
GroupMembers and the list of expensesadd_expense, net_balances, record_payment
ExpensePayer, total in paise, and each person's shareNone; it is a record that never changes
SplitRuleThe method for turning a total into sharesshares(total, members)
EqualSplit, ExactSplit, PercentSplitOne split method eachshares, with their own checks
simplify functionTurning balances into a payment listsimplify(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

Notice 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.

The three split rules on the flat's bills
RuleInputFlat exampleShares (₹)
EqualTotal and peopleGroceries ₹1,000 among A, B, C333.34, 333.33, 333.33
EqualTotal and peopleWi-Fi ₹1,179 among all four294.75 each, no leftover
ExactAn amount per personElectricity ₹2,450 by room meter500, 600, 700, 650
PercentagePercent per person, in basis pointsCab ₹1,457.25 at 40, 30, 30 percent582.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.

python
# 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 out

We 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.

python
# 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 net

Notice 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

Each 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

Every 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.

python
# 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.

python
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.

text
{'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}
Before and after simplification
Raw debtsGreedy plan
Number of payments63
PaymentsA to C, A to D, B to A, B to C, B to D, D to CA pays C ₹710.99, B pays C ₹410.93, B pays D ₹75.33
Each person's totalA owes ₹710.99, B owes ₹486.26Unchanged
Check after payingNot applicableAll 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.

python
# 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 vs exact settle-up
Greedy (largest pays largest)Exact (bitmask search)
PaymentsAt most n minus 1; sometimes one or more above the minimumThe true minimum
Time on our machineUnder 1 second for 100,000 people1.61 seconds for 20 people, doubling per person
How often worseAbove the minimum in 288 of 3,000 random small groups in our testNever
Good forAny group sizeSmall 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.

  1. 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.

  2. 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.

  3. Draw the classes

    User, Group, Expense, SplitRule with three strategies, and a simplify function. Explain why SplitRule is a Strategy.

  4. 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.

  5. 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.

  6. Name the limit

    Say greedy is not always minimal, the exact minimum is NP-hard, and an exact search works only for small groups.

  7. 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

Practise this with Lala

Practise