Splitwise
Design Splitwise expense sharing: exact money splits, balanced ledger, concurrent edits, idempotency and confirmed settlements.
Splitwise: Understand the building blocks
Choose a mechanism because it satisfies a requirement. These explanations describe the exercise design, not undisclosed implementation choices at the named company.
Splitwise: Quick vocabulary
| Term | Meaning and role in this exercise |
|---|---|
| API: Application Programming Interface | The contract for requests, responses and errors between components. An API route does not itself enforce every business invariant. |
| SQL: Structured Query Language | A language for querying/manipulating relational data. Correctness still depends on constraints, transactions and isolation choices. |
| HTTP: Hypertext Transfer Protocol | The request/response protocol used by web APIs and media fetching. Application retries still need safe operation identities. |
Splitwise: Ledger and immutable revisions
What it means: A ledger is authoritative history of recorded monetary or obligation changes. Immutable revisions keep corrections auditable rather than overwriting the past.
Why this problem needs it: Expense edits must preserve a traceable zero-sum history.
How to use it: Commit an expense/revision and its balanced deltas together, use integer minor units with a currency policy, and reconstruct balances from accepted facts.
Example in this design: Shares 334,333,333 for a 1,000 expense produce net balances 666,-333,-333 when A paid.
When it fits: Useful when edits, retries and reconciliation must preserve an explainable balance.
Alternative and limits: Mutable balances can be faster to read but still need safe revision history and reconciliation. Recording a settlement is not proof an external payment occurred.
Splitwise: Relational database transaction and constraints
What it means: A relational store keeps structured rows; a transaction commits a bounded group of changes together. Constraints reject specified invalid states.
Why this problem needs it: Expense, shares, revision and operation result must agree after commit.
How to use it: Use primary/unique keys and a chosen isolation/locking protocol for the actual invariant. Atomically persist related rows and the operation result before acknowledgement.
Example in this design: Reject a set of shares that does not sum to the expense; enforce it within the transaction protocol.
When it fits: Useful for bounded cross-row correctness, joins and access paths that fit the relational model.
Alternative and limits: Key/value stores can fit simpler keyed operations. “SQL” alone does not prevent lost updates, validate every cross-row rule or coordinate an external provider.
Splitwise: Compare-and-set (CAS) and optimistic concurrency
What it means: A conditional write changes state only if an expected version or value still matches. The comparison and write share one atomic boundary.
Why this problem needs it: Two edits based on the same revision must not silently overwrite each other.
How to use it: Carry the version read by the caller; atomically compare, mutate and advance it. On conflict, re-read or reject under the business policy.
Example in this design: One expected-revision update succeeds and the competing stale edit conflicts.
When it fits: Useful when concurrent requests must not silently overwrite the same owned state.
Alternative and limits: Row locks or a serial owner can simplify highly contended updates. A read followed by an unconditional write is not CAS.
Splitwise: Projection and materialized view
What it means: A projection is a derived read model built from authoritative facts; it improves a particular read path and can be rebuilt.
Why this problem needs it: Fast balance reads can be rebuilt from accepted expense facts.
How to use it: Consume versioned facts with replay-safe updates, expose a watermark/revision, and compare or rebuild from the authoritative history.
Example in this design: After editing to 1,200, the net correction is 134,-67,-67; replay applies that revision once.
When it fits: Useful when read shapes or volume justify duplicating computation/state.
Alternative and limits: Read authoritative rows directly for a simpler strongly current baseline. A stale projection must not grant revoked access or decide a strict inventory transition.
Read the diagram
Ledger records expense 1000 paid by A; Shares 334,333,333 conserve the exact total; Net balances are 666,-333,-333: sum zero; Transaction and request identity prevent double posting
Define expense sharing before designing payments
For “design Splitwise,” ask whether the system records shared expenses and settlements or actually transfers money. This original exercise records group expenses, per-currency obligations and confirmed manual settlements. It does not initiate bank transfers. A payment integration introduces provider idempotency, unknown outcomes and reconciliation; compare Payment System. Product names are prompts, not private implementation claims.
A share is the part of an expense a participant owes. A ledger is authoritative history from which balances can be reconstructed. A projection is a convenient derived view. A settlement records a reduction of an obligation under a declared confirmation policy. A record saying “paid” is not independent evidence that money moved outside the application.
Ask the questions that change correctness
| Ask | Assumption for the worked design | Consequence |
|---|---|---|
| Groups, individual expenses or both? | Groups with bounded membership | Group identity and permission scope every query/write |
| Equal, exact, percentage or weighted splits? | Equal and exact amounts first | Validation and deterministic rounding depend on split type |
| One or several payers? | One payer in the baseline | Multiple payers add contributions whose total must equal the expense |
| Currency and conversion? | Separate currency ledgers, no automatic conversion | Do not add INR and USD or infer an exchange rate |
| Edits/deletes and audit history? | Versioned corrections, retained history | Reversal/replacement must be atomic and authorized |
| Settlements versus real transfer? | Confirmed manual record; no bank charge | Provider results are outside the current system boundary |
| Membership changes and privacy? | Current members may read; removed members' obligations remain under explicit policy | Access revocation is different from debt cancellation |
| Scale, skew and freshness? | One million expenses/day, groups capped at 100 in this exercise | Atomic owner and bounded reads before extra services |
Open: “I’ll design recording and correcting shared expenses and tracking per-currency balances. I’ll exclude actual transfer and currency conversion unless you want those as deep dives. How should rounding and settlement confirmation work?”
State the invariants and round deliberately
Every expense has an integer amount in the currency's declared minor unit. Do not hard-code two decimal places for all currencies. Participant shares sum exactly to that amount. Ledger deltas sum to zero within each expense revision/currency. The payer's credit and participants' debits are facts of the same committed operation. Replaying one request does not create another expense.
In this illustrative currency, 1,000 minor units split equally among A, B and C produces shares A=334, B=333, C=333. Divide using integer quotient 333 and distribute the remainder 1 in a documented stable participant order. If A pays the entire expense, net deltas are A=+666, B=-333, C=-333. A also owes its own share, so A is not credited the full 1,000 as a net balance.
Read the diagram
A pays 1000; A, B and C share equally → Integer quotient is 333; remainder is 1 → Stable participant order assigns shares 334, 333, 333 → Net = paid − owed: A +666, B −333, C −333 → Conservation check: 666 − 333 − 333 = 0 → Currency scale is declared; do not assume every currency has cents
Exact splits reject totals other than the expense amount. Percentage/weighted splits require a declared input precision and remainder policy rather than floating-point summation. Zero/negative amounts, invalid currency, duplicate participant IDs and nonmember participants need explicit validation. Refund/correction behavior is a separate operation, not an unexplained negative expense trick.
Model immutable facts and revision ownership
groups(group_id,version)
memberships(group_id,user_id,status,role)
expenses(expense_id,group_id,currency,amount_minor,payer_id,current_revision)
expense_revisions(expense_id,revision,actor_id,request_id,state)
ledger_entries(expense_id,revision,user_id,currency,delta_minor)
request_keys(group_id,actor_id,request_key,request_hash,result_id)
settlements(settlement_id,group_id,currency,from_user,to_user,amount_minor,state)POST /groups/{id}/expenses {requestKey,currency,amountMinor,payer,shares}
PUT /expenses/{id} {requestKey,expectedRevision,newAmount,newShares}
GET /groups/{id}/ledger?currency=...&cursor=...&limit=...
POST /groups/{id}/settlements {requestKey,currency,from,to,amountMinor}
GET /groups/{id}/balances?currency=... -> balances, projectionRevisionAuthenticate the caller and authorize the group/object operation. Atomically validate membership policy, claim request identity, insert the revision and ledger entries, and advance the current expense revision. Unique identities and constraints protect local boundaries; cross-row share conservation needs a transaction/procedure or another enforcing owner, not a CHECK on one unrelated row. Select a relational store for these bounded transactions as a baseline; name the constraints/access paths rather than claiming every SQL database transaction prevents every anomaly.
One million expenses/day is about 11.6 writes/s average, 116/s at a 10× peak. At an illustrative 2 KB per expense/history footprint before further corrections, raw growth is about 2 GB/day; replica/index overhead and retention are separate. Large groups, repeated edits and ledger reads can dominate differently, so state the group cap and bounded pagination instead of sharding reflexively.
Follow create, correct and read
Read the diagram
Client sends group-scoped request key and expense payload → Transaction checks membership, key payload and expected revision → Insert balanced immutable postings, revision and outbox event → Commit all rows together; timeout leaves outcome unknown → Same key retrieves committed result; conflicting payload rejects → Projection consumes unique event IDs; rebuild from ledger if needed
A create retry with the same caller-scoped key and request fingerprint returns the original expense result. Reusing that key with different shares is a conflict. A balance cache is keyed by group, currency and applicable policy/revision. It must not bypass current authorization. The response exposes its projection revision when lag is permitted; a correctness-critical settlement decision consults the authority or a projection proven current under the chosen contract.
To correct expense e7 revision 1 to revision 2, reverse the old effects and post the new effects atomically, preserving the audit history. For the 1,000 example, replacing the amount with 1,200 and equal shares changes A's net credit from 666 to 800 and B/C from -333 to -400. The adjustment is +134,-67,-67, summing to zero. A second editor submitting expectedRevision=1 after revision 2 commits gets a conflict, not an extra reversal.
Read the diagram
Revision 1 nets: A +666, B −333, C −333 → New amount 1200; each share 400; nets +800, −400, −400 → Adjustment postings: A +134, B −67, C −67 → Adjustment sums to zero; original history remains intact → Expected revision 1 advances atomically to revision 2 → Another edit with expected revision 1 conflicts; reload and retry
Simplify debts without inventing money
Net balances A=+666, B=-333, C=-333 can be represented as B owing A 333 and C owing A 333. A simplification proposes transfers preserving each person's net per-currency obligation. It does not prove a minimum number of transfers for every general problem, and it must respect any agreed restrictions on counterparties/groups. Do not simplify across currencies or groups whose obligations may not legally/product-wise be netted.
A confirmed manual settlement B→A of 100 reduces B's debt by 100 and A's credit by 100: new net balances A=+566, B=-233, C=-333. Stable settlement identity prevents replay from applying it twice. Define who can confirm, how the other party disputes it and whether overpayment is permitted. Do not silently let an untrusted client declare arbitrary other members' debts paid.
If real payment is added, record intent and an explicit pending/unknown/succeeded/failed outcome. A provider timeout is unknown; retry/reconcile the same provider identity. Do not modify the authoritative debt as definitively paid merely because an HTTP request was sent. That is a new product boundary, not a small UI feature.
Handle failures and operate safely
| Failure | Required behavior |
|---|---|
| Response lost after create commit | Retry returns the existing expense; history is not duplicated |
| Two editors read revision 1 | One conditional correction wins; loser rereads and resolves conflict |
| Projection worker replays an event | Revision/entry identity prevents an extra balance effect |
| Projection is rebuilt while edits continue | Capture a source position, replay through a known boundary, verify before switching |
| Member removed with an unpaid share | Preserve the agreed financial record; revoke access under a separate policy |
| Settlement confirmation duplicated | One ledger effect per settlement identity |
Track rejected invalid splits, conservation violations, stale-edit conflicts, projection age and reconciliation differences. Use a bounded ledger rebuild/checkpoint process. Alert on a mismatch between authoritative facts and balances rather than “repairing” it with an untraceable mutable total. Group-scoped access logs should avoid logging sensitive expense descriptions or authentication tokens. Storage, history growth and projection reads are the baseline cost drivers.
Foundation, senior and staff explanations
Foundation: define shares/ledger, compute the 1,000-unit example, draw an authorized write/read and explain replay. Intermediate: add pagination, revision conflicts and cache freshness. Senior: show an atomic correction, a recoverable projection and settlement uncertainty; identify authorization and observability boundaries. Staff: compare synchronous balances against asynchronous projections for actual workloads, plan migration/reconciliation with no double posting, define group-domain ownership and specify rollback/read cutover gates.
Practice before revealing the answer
Deliver clarifying questions, the three conservation/replay invariants, record/API model, a complete create-and-read path and the e7 correction trace. Add an edit arriving during a balance rebuild. For staff depth, require that old and new clients continue during a schema migration.
Question: Is giving A a net credit of 1,000 correct when A also owes an equal share?
Show answer and explanation
Answer: No. A's own share is 334, so A's net credit is 666. B and C owe 333 each. All deltas must reconcile to zero.
Question: Can a client set the cached balance to its newly calculated total after editing?
Show answer and explanation
Answer: That can overwrite another committed change or trust unauthorized input. The ledger revision owner validates and commits the correction; projections follow a replay-safe protocol.
Primary documentation: PostgreSQL constraints and transaction isolation. These explain selected database mechanisms; the ledger policy, rounding and scale are independent exercise assumptions.
Decision review: Splitwise
Splitwise: What it is
An expense-sharing design records who paid and who owes under an explicit currency, rounding and revision policy.
Splitwise: Why it is needed
Balances are derived views; rounding and edits must preserve per-currency conservation and author authorization.
Splitwise: When to use it
Use immutable expense facts and balanced adjustments when history, auditability or correction matters; distinguish recording settlement from actually moving money.
Splitwise: How to use it
Compute shares in integer minor units using a deterministic remainder rule. Atomically publish a revision and its balanced adjustments. Rebuild balances from applicable facts, without combining different currencies silently.
Splitwise: Compare an alternative
Overwriting an expense is simpler but loses history unless another audit mechanism preserves it. Automatic cross-currency settlement introduces exchange-rate and fee contracts outside ordinary expense splitting.
Splitwise: Worked decision exercise
The following inputs and policies are original exercise assumptions; they are not measurements or undisclosed company behavior.
A pays 1,000 units shared equally with B and C. Assign shares 334,333,333 in a stable order; nets are +666,-333,-333. Editing the amount to 1,200 gives +800,-400,-400, so the balanced adjustment is +134,-67,-67.
Read the diagram
1000 split as 334,333,333: nets +666,-333,-333; Edit to 1200: nets +800,-400,-400; Adjustment +134,-67,-67 sums to zero; Expected revision prevents two conflicting edits
Can two editors both publish a correction based on revision 1?
Show answer and explanation
Answer: Not under an expected-revision rule: one advances the revision and the other receives a conflict. A retry with the same operation ID returns its committed result. The adjustment sums to zero, and recording a settlement does not prove a provider transferred money.
Splitwise: Check the design
Verify rounding, revision replay, membership changes and independent currency ledgers. Compare a rebuilt balance projection with canonical expense facts.