# Splitwise

Course: [Low-Level Design](https://www.devtutorials.ai/learn/system-design/lld) · Section: LLD Case Studies
Source: https://www.devtutorials.ai/learn/system-design/lld/case-studies/splitwise

> Design an expense splitter: equal, exact and percent splits in whole rupees, net balances, and settling up with few payments.

Five friends go on a trip and share several expenses. One person pays for the hotel, another pays for dinner, and someone else pays for the taxi. By the end of the trip, it becomes difficult to remember how much each person paid and who owes money.

An expense-sharing system solves this problem by recording shared expenses and calculating each person’s final balance.

**In simple words:** you will combine many shared expenses into one final balance for each person, then calculate the payments needed to settle the group.

## What You Will Learn

In this case study, you will:

1. Keep one net balance per person instead of every debt.
2. Split bills equally, exactly or by percent, without losing a rupee.
3. Refuse splits that do not add up.
4. Settle all debts with a simple greedy rule.
5. Build the ledger in the sandbox and pass the hidden tests.

The big idea is to **simplify the data**. Many bills look complicated, but one net number per person is all you need to settle up.

## Step 1: Agree on the Requirements

Before writing code, confirm what the expense-sharing system must do.

### What It Must Do

- Add an expense using `addExpense(paidBy, amount, split)`.
- Support equal, exact, and percentage-based splits.
- Use whole rupees and ensure that every split adds up to the full expense amount.
- Use `balances()` to show how much each person should receive or pay.
- Leave people with a zero balance out of the result.
- Use `settleUp()` to calculate the payments needed to bring every balance to zero.
- Cancel debts that balance each other, including circular debts.

A positive balance means that the person should receive money. A negative balance means that the person owes money.

### What We Leave Out

- Groups, currencies and receipts.
- The very fewest payments in every case (that problem is very hard; greedy is close and simple).
- Editing or deleting an expense.

Leaving things out on purpose is not lazy. It keeps the design small enough to finish, and you can say how you would add them later.

## Step 2: Find the Classes

A simple trick: underline the **nouns** in the requirements. Most of them become classes. The **verbs** become methods.

| Class | Its one job |
|---|---|
| Ledger | Records expenses and keeps one net balance per person |
| Split | Says how one bill is shared: equal, exact or percent |
| shares() | Turns an amount and a split into whole-rupee shares |

<LldDiagram caption="The class diagram for this design." code={"classDiagram\n  class Ledger {\n    -net: Map~string, number~\n    +`addExpense(paidBy, amount, split)` void\n    +balances() Record\n    +settleUp() string[]\n  }\n  class Split {\n    <<union>>\n    equal\n    exact\n    percent\n  }\n  Ledger ..> Split : reads"} />

## Step 3: Build the Key Parts

### Keep One Number per Person

The system could store every individual debt separately, such as “Ravi owes Asha 300”. However, as more expenses are added, this creates many records that are difficult to settle.

A simpler approach is to keep one net balance for each person:

**Net balance = total amount paid − total share owed**

- A positive balance means the person should receive money.
- A negative balance means the person owes money.
- A zero balance means the person has paid exactly their share.

For example, if Asha pays 900 for a bill shared equally among Asha, Ravi, and Meena, each person’s share is 300:

- Asha’s balance is 900 - 300 = 600.
- Ravi’s balance is 0 - 300 = -300.
- Meena’s balance is 0 - 300 = -300.

The balances add up to zero: `600 + (-300) + (-300) = 0`.

Step through the dinner bill:

Here is the same idea in code:

```ts runnable title="Only the net balance per person matters"
const net = new Map<string, number>();
function add(p: string, v: number): void { net.set(p, (net.get(p) ?? 0) + v); }

// Asha paid 900 for dinner for three.
add("Asha", 900);
for (const p of ["Asha", "Ravi", "Meena"]) add(p, -300);

console.log(Object.fromEntries(net));
```

Expected output:

```text
{
  "Asha": 600,
  "Ravi": -300,
  "Meena": -300
}
```

### Splitting Without Losing a Rupee

100 split three ways is 33.33. Rupee amounts must be whole, so give the leftover rupee to someone.

The figure shows how 100 is shared between Asha, Ravi and Meena:

Here is the same idea in code:

```ts runnable title="An equal split that loses no rupee"
function equalShares(amount: number, people: string[]): Record<string, number> {
  const base = Math.floor(amount / people.length);
  let extra = amount - base * people.length; // leftover rupees
  const out: Record<string, number> = {};
  for (const p of people) {
    out[p] = base + (extra > 0 ? 1 : 0);
    if (extra > 0) extra--;
  }
  return out;
}

const s = equalShares(100, ["Asha", "Ravi", "Meena"]);
console.log(s);
console.log("adds up to", Object.values(s).reduce((a, b) => a + b, 0));
```

Expected output:

```text
{
  "Asha": 34,
  "Ravi": 33,
  "Meena": 33
}
adds up to 100
```

Never store money as fractions like 33.333. Rounding each share separately can lose or invent a rupee, and then the balances no longer add up to zero. Use whole rupees (or paise) and hand out the leftover on purpose.

### Three Kinds of Split

Bills are split equally, by exact amounts, or by percent. A TypeScript **union type** lists the three shapes, and TypeScript checks that each one is handled.

| Kind | What it holds | Example |
|---|---|---|
| `equal` | a list of people | equal between 2 people (Asha, Ravi) |
| `exact` | an amount for each person | exact: Asha 200, Ravi 300 |
| `percent` | a percent for each person | percent: Asha 60%, Ravi 40% |

Here is the code:

```ts runnable title="Three ways to split, behind one function"
type Split =
  | { kind: "equal"; among: string[] }
  | { kind: "exact"; amounts: Record<string, number> }
  | { kind: "percent"; percents: Record<string, number> };

function describe(split: Split): string {
  switch (split.kind) {
    case "equal": return `equal between ${split.among.length} people`;
    case "exact": return `exact: ${Object.keys(split.amounts).join(", ")}`;
    case "percent": return `percent: ${Object.entries(split.percents).map(([p, v]) => p + " " + v + "%").join(", ")}`;
  }
}

console.log(describe({ kind: "equal", among: ["Asha", "Ravi"] }));
console.log(describe({ kind: "percent", percents: { Asha: 60, Ravi: 40 } }));
```

Expected output:

```text
equal between 2 people
percent: Asha 60%, Ravi 40%
```

The `switch` here is on a **tagged union**, which TypeScript checks for missing cases. That is fine for three fixed shapes. If new split kinds kept arriving, each one would become a strategy class.

### Check Before You Accept

An exact split must add up to the bill. Step through two splits of a 500 bill:

| Split | Result |
|---|---|
| Asha 200, Ravi 300 | ok |
| Asha 200, Ravi 200 | refused: shares add up to 400, but the bill is 500 |

Here is the same idea in code:

```ts runnable title="Check the split before accepting it"
function checkExact(amount: number, amounts: Record<string, number>): string {
  const sum = Object.values(amounts).reduce((a, b) => a + b, 0);
  return sum === amount ? "ok" : `shares add up to ${sum}, but the bill is ${amount}`;
}

console.log(checkExact(500, { Asha: 200, Ravi: 300 }));
console.log(checkExact(500, { Asha: 200, Ravi: 200 }));
```

Expected output:

```text
ok
shares add up to 400, but the bill is 500
```

### Settling Up

The greedy rule: the person who owes the most pays the person who should receive the most. Transfer as much as possible, update both balances, and repeat until every balance becomes zero.

Step through the balances Asha +500, Ravi −100, Meena −250 and Kiran −150:

The payments are: Meena pays Asha 250, Kiran pays Asha 150, Ravi pays Asha 100.

Here is the same idea in code:

```ts runnable title="Settle up: biggest debtor pays biggest creditor"
function settle(balances: Record<string, number>): string[] {
  const owe = Object.entries(balances).filter(([, v]) => v < 0).map(([p, v]) => ({ p, v: -v }));
  const get = Object.entries(balances).filter(([, v]) => v > 0).map(([p, v]) => ({ p, v }));
  const pay: string[] = [];
  while (owe.length && get.length) {
    owe.sort((a, b) => b.v - a.v);
    get.sort((a, b) => b.v - a.v);
    const x = Math.min(owe[0].v, get[0].v);
    pay.push(`${owe[0].p} pays ${get[0].p} ${x}`);
    owe[0].v -= x;
    get[0].v -= x;
    if (owe[0].v === 0) owe.shift();
    if (get[0].v === 0) get.shift();
  }
  return pay;
}

for (const line of settle({ Asha: 500, Ravi: -100, Meena: -250, Kiran: -150 })) console.log(line);
```

Expected output:

```text
Meena pays Asha 250
Kiran pays Asha 150
Ravi pays Asha 100
```

Four people needed only three payments, not one per bill.

**Easy way to remember:** forget the bills. Add up **one net number per person**. Then the **biggest debtor pays the biggest creditor** until everyone is at zero.

## Lab: Build It

Build the `Ledger`. The hidden tests add equal, exact and percent bills, check the balances, check that bad splits are refused, and check the settle-up payments.

1. Write a `shares(amount, split)` function for the three split kinds. Refuse splits that do not add up.
2. `addExpense` adds the amount to the payer and subtracts each share. Keep one net number per person.
3. `settleUp` repeatedly lets the biggest debtor pay the biggest creditor (break ties by name), recording `from->to:amount`.

Open the lab in the sandbox. Run the tests after each step. A failing test names the situation that broke, not the test code.

## Predict the Output

<CsPredict id="split-1" code={"var net = new Dictionary<string, int> { [\"Asha\"] = 0, [\"Ravi\"] = 0 };\nnet[\"Asha\"] += 400;\nnet[\"Asha\"] -= 200;\nnet[\"Ravi\"] -= 200;\nnet[\"Ravi\"] += 200;\nnet[\"Asha\"] -= 100;\nnet[\"Ravi\"] -= 100;\nConsole.WriteLine($\"{net[\"Asha\"]} {net[\"Ravi\"]}\");"} expected={"100 -100"} why="Asha paid 400 and owes 200 + 100 = 300, so she is at +100. Ravi paid 200 and owes 200 + 100 = 300, so he is at -100. The two balances add up to zero." />

## Common Mistakes

| Mistake | Wrong | Correct | Why it happens |
|---|---|---|---|
| Storing every debt | A list of who owes whom for each bill. | One net balance per person. | It mirrors how people talk about bills. |
| Fractions of rupees | Shares of 33.333. | Whole rupees; hand out the leftover. | Division gives fractions by default. |
| Unchecked splits | Exact amounts that add up to 490 for a 500 bill. | Refuse the expense. | Users make typos; tests rarely do. |
| Settling per bill | One payment for every expense. | Net first, then settle. | It feels fairer to repay each bill. |
| Order-dependent output | Payments come out in a different order each run. | Sort ties by name. | Map order depends on insertion order. |

## Practice

## What to Remember

- Keep **one net number** per person.
- Balances always **add up to zero**.
- Use **whole rupees** and give out the leftover on purpose.
- **Refuse** splits that do not add up.
- Settle up: **biggest debtor pays biggest creditor**.

## Further Reading

These are the sources used to check this case study:

- [Splitwise: How simplify debts works](https://blog.splitwise.com/2012/09/14/debts-made-simple/): the idea behind simplifying.
- [TypeScript Handbook: Discriminated unions](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions): the Split type.
- [MDN: Math.floor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor): whole-rupee shares.

Next: **Library System**. You will build lending, due dates, fines and a waiting list.
