Why this matters: every variation in this lesson is a question your design either absorbs or fumbles, and the difference traces back to the same two decisions from lesson 02 — record-first data, and one write path. Rehearse these and you're not memorizing answers; you're watching your own seams get exercised.
Rounding policies: determinism is the requirement
The sums-exactly invariant says the leftover paise must go somewhere. Where is policy, and there are several defensible ones:
first-in-order sorted participants; first `leftover` pay +1 (simplest)
payer-absorbs the payer eats the remainder (feels fair)
largest-remainder for percentage splits: round down, then give
+1 to the shares with the largest fractional
remainders until the total reconciles
rotating spread the extra paisa across expenses over time (stateful — rarely worth it)
The non-negotiable across all of them is determinism: the same expense must produce the same shares on every run, or your tests flap and replaying the record produces a different ledger than the one users saw. That last clause is the deep one — in a record-first design, share computation is part of derivation, and derivation must be a pure function. Say that sentence in an interview and the rounding discussion is over.
Multi-currency: which seam takes the hit
Single currency today, but the question is a when, not an if. Walk where it lands:
- Money grows a currency: an amount becomes
(currency, minorUnits)— and "minor units" itself is per-currency (JPY has no subunit; KWD has three decimal places). The boundary parser consults a currency table instead of assuming 100. - The ledger key grows a currency: you owe someone euros and rupees as two cells, because netting across currencies requires an exchange rate, and a rate is an opinion with a timestamp.
- The record stores the original currency — always. Converting at write time bakes one day's rate into your source of truth; converting at display time keeps the record honest and moves rates to the read side, where opinions belong.
Notice what didn't change: the split hierarchy (it works in minor units of whatever currency), the write path, settle-up's shape (per-currency plans). The variation lands in the Money type and the ledger keying — narrow, nameable surfaces. That's the answer format an interviewer wants: which seams, and why the rest is untouched.
Groups: a scope, not a wallet
Lesson 01's requirement said it, and the design should honor it literally: a group is a filter over the same record — expenses tagged with a group id, balance views scoped to it — not a separate ledger with its own money. The payoff shows when membership changes mid-life: someone joins the flat after ten expenses. Do they retroactively owe? Of course not — and a scope-based design gets that for free, because they simply appear in no prior expense. A wallet-based design has to migrate something, and anything that migrates money is a bug farm.
The one real design question groups add: does a pairwise balance between two users net across groups or stay per-group? Both are defensible products; the ledger supports either by keying with or without the group id. Name the choice — don't let it happen to you.
Idempotent posting: the retry that double-charges dinner
A phone posts addExpense, times out, retries — and without protection, dinner exists twice and every balance is wrong. The standard fix: the client generates an expense id (a UUID minted on the device), and addExpense becomes idempotent — a second arrival of the same id is acknowledged and ignored.
Two design notes worth saying. The dedupe check belongs at the write path's front door — one write path means one place to enforce it, which is the lesson-02 decision paying rent again. And if a duplicate ever does slip through, a record-first design can be repaired: find the duplicate entry, append a correction, re-derive. The balances-only design discovers the double-charge as an unexplainable number.
Edits and deletes: corrections, not surgery
We scoped edits out, but designed "as if edits will come." Here's what that pressure produces. In a record-first system, an edit is not an in-place mutation of an old expense — mutating history changes what balances were, silently, and destroys the property that the record explains the present. Instead:
- A delete appends a reversal entry (the expense's exact negative), flowing through the ordinary write path.
- An edit is a reversal plus a corrected expense — two entries, one logical operation, with the original still visible and marked superseded.
This is the same discipline accountants have used for centuries (nobody erases a journal line; they post a correcting one), and it's what "immutable record" buys you when reality gets messy. The seam you left in lesson 01 turns out to be no new machinery at all — just permission to append two entries instead of one.
Key takeaway
Splitwise's variations all resolve against the same seams: rounding is a deterministic policy inside share derivation, currency lands in the Money type and ledger keying while the record keeps original amounts, groups stay scopes over one record, retries are defused by client-generated ids at the single write path, and edits become append-only corrections. If a change can't be named to a seam like that, the design — not the change — is the problem.