Articles
Splitwise Low-Level Design Interview: Expense Splits, Balances, and Settlement
Practice a Splitwise low-level design interview with exact expense shares, cent rounding, ledger balances, idempotency, edits, and valid settlement plans.
A Splitwise low-level design interview is a money-conservation problem before it is a class-diagram problem. Normalize the expense into exact paid and owed shares, derive signed balances, and treat repayment as a separate operation. A clean interface cannot compensate for a missing cent, a repeated expense, or a settlement algorithm advertised as minimal without proof.
This guide proposes an original Splitwise-style exercise. Official Splitwise API documentation informs the public field semantics; it does not reveal the company's internal architecture. The classes, rounding policy, ledger, and settlement examples below are our design recommendations. We do not claim that Splitwise or another employer uses this exact interview prompt.
What contract should you establish before drawing classes?
For this exercise, users belong to groups and can record expenses with one or several payers. An expense has a currency, a positive total, participants, paid shares, owed shares, and a stable identifier. We support equal, exact, and percentage splits. Recording an expense changes the group's ledger; recording a repayment changes it again.
The official Splitwise API exposes an expense cost, currency code, and participant paid and owed shares. Its create-expense documentation describes decimal-string costs and distinguishes equal group splitting from supplying individual shares. Those are public API facts, not evidence of a particular database schema. Official Splitwise developer API.
Our smaller contract uses USD cents as integers. This assumes a two-decimal currency; a production money abstraction must obtain the appropriate scale for each supported currency. Keep currency in the value object and reject arithmetic across currencies. Do not silently convert EUR into USD or combine their balances.
Also ask whether settlements may redirect debts between members. A valid net settlement can change who pays whom while preserving each person's final position. If the product requires original bilateral obligations or member consent, that freedom disappears. Decide this before choosing a settlement algorithm.
How do equal, exact, and percentage splits conserve money?
The invariant is simple: the sum of paid shares and the sum of owed shares must each equal the expense total. Validate both independently. A single payer's contribution does not imply that the payer owes nothing; a payer can also consume part of the shared expense.
For an equal split of USD 10.00 among A, B, and C, integer division gives 333 cents each with one cent remaining. Our deterministic policy awards remaining cents by sorted participant identifier, yielding A=334, B=333, C=333. Another stable policy could be reasonable, but unspecified iteration order is not an accounting policy.
Here is a minimal original implementation for nonnegative minor units and unique participant identifiers:
def equal_shares(total, members):
if total < 0 or not members:
raise ValueError("invalid split")
ordered = sorted(members)
if len(set(ordered)) != len(ordered):
raise ValueError("duplicate participant")
base, remainder = divmod(total, len(ordered))
return {
member: base + (index < remainder)
for index, member in enumerate(ordered)
}
The expense boundary additionally requires a strictly positive total. Allowing zero in this pure allocation helper does not mean zero-cost expenses are permitted by our application contract. Separate a reusable arithmetic function from product validation.
For exact splitting, accept nonnegative integer shares only when their sum equals the total. Reject duplicate participants and shares assigned outside the authorized group. For percentage splitting, use nonnegative integer basis points totaling 10,000, where 100 basis points equal one percent. Avoid computing each share with binary floating point and hoping the rounded values happen to add up.
Our percentage policy floors each exact proportional share, then distributes leftover cents by largest fractional remainder, breaking ties by participant identifier. Splitting 1,001 cents at 50%, 30%, and 20% starts with 500, 300, and 200 cents. The remaining cent goes to the 50% participant, producing 501, 300, and 200. The policy and its tie-break must survive retries unchanged.
What is the smallest useful domain model?
Favor classes that own an invariant rather than a class for every noun. Money owns amount and currency. SplitPolicy converts validated input into normalized owed shares. Expense records those shares and the paid contributions. LedgerEntry records the resulting signed posting. Settlement records an actual repayment separately.
An ExpenseService coordinates membership checks, authorization, idempotency, and atomic persistence. It asks the split policy for normalized shares, validates conservation, and writes the expense with its postings. A repository stores records; it should not invent rounding rules while saving them.
Keep the canonical stored representation consistent across policies. Equal and percentage inputs are useful for explanation and later editing, but the ledger consumes exact normalized minor-unit shares. Store the policy version and original input alongside those shares so an old expense can be explained after the rounding policy changes.
Return a structured result containing the expense identifier, version, normalized shares, and resulting operation status. A boolean hides the values the UI needs to show and the identifier a retry needs to reuse. Conversely, returning every historical group expense makes a simple write response unnecessarily large.
How do paid and owed shares become balances?
Define the sign convention once: a positive balance means the member should receive money; a negative balance means the member should pay. Each expense contributes paid_share - owed_share to that member. The group sum must remain zero within each currency.
In our USD 90.00 dinner example, A pays all 9,000 cents and three members owe 3,000 each. The postings are A=+6,000, B=-3,000, C=-3,000. A is credited only for the amount paid on others' behalf.
Now change the payers: A pays 6,000 and B pays 3,000 while each still owes 3,000. The postings become A=+3,000, B=0, C=-3,000. A design that assumes one payer would lose this distinction or require artificial duplicate expenses.
For the first example, B's repayment of 3,000 to A adds +3,000 to B's balance and -3,000 to A's. C's matching repayment closes the remaining balances. Treating these repayments as new equal-split dinners would create new obligations and corrupt the result.
Maintain postings as auditable records. A cached balance is a projection that should reconcile with the sum of posted entries. If they disagree, identify the missing or duplicated operation before patching the number shown on the screen.
How should retries and concurrent writes behave?
Our write contract uses an idempotency key scoped to the actor and operation. The first accepted request stores its normalized payload hash and result. Repeating that key with the same payload returns the original result. Reusing it with different amounts returns a conflict instead of silently changing the existing expense.
The expense and its ledger postings must commit atomically. Otherwise, a crash after saving the expense but before saving the postings leaves history and balances inconsistent. Enforce uniqueness for the idempotency record in persistence; checking for a key in application memory alone cannot prevent two concurrent requests from both committing.
For operations that depend on the current group state, our design uses a group or record version. An edit supplies the version it read; a conflicting update must be resolved before another write is accepted. Ordinary independent expense inserts may use a less restrictive concurrency model, provided their postings remain atomic and uniquely recorded.
If the UI shows a cached balance, say when it refreshes and whether it can be temporarily stale. A stale read is different from a duplicate committed expense. Retrying the write to repair the screen can make matters worse unless the idempotency contract is enforced.
The official API also warns that HTTP 200 alone does not prove create-expense success: callers must inspect the returned errors. That is a documented integration detail. Our internal service still needs its own explicit success and rejection result contract.
How do edits and deletion preserve history?
For this exercise, an edit reverses the previous expense's postings and posts the replacement in one transaction. Preserve the original identifier, record a new version, and link the reversal to the old version. A deletion similarly records a reversal instead of erasing the evidence that created the balance.
Suppose the USD 90 dinner was actually USD 75. Reverse +60/-30/-30 dollars, then post the corrected equal shares: +50/-25/-25. The net balance change is A=-10, B=+5, C=+5 dollars. Recomputing only A's displayed balance would leave the group sum inconsistent.
An edit after a repayment needs an explicit product policy. Our model preserves both events, so the correction can legitimately create a new balance between previously settled members. It does not rewrite a completed repayment as though it never happened. Explain this behavior in the UI and retain authorization and audit records.
Notifications should follow the committed operation and carry its identifier. If notification delivery retries, that should not repost the expense. Treat the database result and the external notification as separate concerns connected by a reliable event record.
Does a greedy settlement plan minimize transfers?
A common greedy plan repeatedly matches the largest debtor with the largest creditor, transfers the smaller outstanding amount, and updates both balances. Under unrestricted net settlement it conserves money and clears at least one nonzero balance per transfer. It is a useful valid-plan algorithm. Validity does not establish minimum transfer count.
Here is our counterexample in arbitrary whole monetary units. Creditors have +8, +7, and +6; debtors have -9, -8, and -4. Largest-first greedy makes transfers of 8, 7, 4, 1, and 1: five transfers.
A different plan uses four: the -8 debtor pays the +8 creditor; the -9 debtor pays 7 to the +7 creditor and 2 to the +6 creditor; the -4 debtor pays the remaining 4 to that creditor. Every balance closes. This proves that the greedy five-transfer result is not minimal for this input.
If the prompt explicitly requests minimum transactions, discuss a search over nonzero balances with pruning, its input-size limits, and how you verify the result. Do not disguise a valid heuristic with a method name such as minimumTransfers. For a large practical group, a transparent valid plan may be the accepted requirement, but that is a scope decision to confirm.
Also distinguish net positions from bilateral history. Two groups can have the same net balances but different original payer-to-participant edges. The net algorithm preserves final positions, not every historical edge. If edges are protected constraints, incorporate them and report when the unrestricted plan is inadmissible.
Practice expense modeling and settlement reasoning
| Question | Follow-up for this design |
|---|---|
| Compute minimal transfers to settle group expenses | Use the counterexample to separate a valid plan from a minimum. |
| Settle Group Expenses with Transfers | Derive signed balances before constructing transfers. |
| Design an Extensible Expense Rule Engine | Keep policy evaluation separate from ledger posting. |
| Aggregate expenses by person, trip, and category | Explain how a projection reconciles with canonical records. |
| Design expense rules engine and return type | Return identifiers and structured explanations to the UI. |
Rehearse one expense, one identical retry, one edit, and one repayment. Check conservation after every committed operation. Then choose another Software Engineer System Design question and defend which layer owns each invariant.
Sources and Further Reading
Research checked October 4, 2026. Integer allocations, ledger transitions, and the greedy counterexample were checked locally. No claim about Splitwise's production storage or settlement implementation is made.