Course contents

LLD problem — Splitwise

Sign in to run this lab and save your progress

Learn

Before you start

Splitwise-style expense splitting is a common design interview problem. Small enough for one class, complex enough to reveal whether you can factor cleanly.

The signals

Two are baked into the requirements:

  1. Multiple split types (equal, exact, percent) → Strategy pattern. Each split algorithm is its own class.
  2. Balance queries must be O(1) → keep a running balance map; don't walk the expense log every time someone asks.

Your design.md should name both.

The balance-representation decision

You have two natural ways to store the balance sheet:

Option A — single-direction storage:

balances[("alice", "bob")] = 50   # alice owes bob 50 (alphabetical)

Good: half the memory. Antisymmetric by construction (the other direction is just the negative). No "did I forget to update both sides" bug.

Bad: query lookup has to sort the pair every time.

Option B — double-entry (write both sides):

balances[("alice", "bob")] = 50
balances[("bob", "alice")] = -50

Good: direct O(1) lookup either way.

Bad: two writes per update; risk of drift if a bug forgets to update the paired entry.

Both are fine. Pin your choice in design.md.

The split-type strategy shape

Naive:

def add_expense(...):
    if split_type == "equal":
        ...
    elif split_type == "exact":
        ...

Strategy:

class EqualSplit(SplitStrategy):
    def compute(self, amount, participants, shares):
        return {p: amount // len(participants) for p in participants}

class ExactSplit(SplitStrategy):
    def compute(self, amount, participants, shares):
        if sum(shares.values()) != amount:
            raise ValueError(...)
        return shares

# Adding "by_ratio" = one new class, no changes to Splitwise.

This is OCP applied to the split algorithm. Adding a new type becomes one new class.

The rules discipline

Three rules matter:

  1. balance(a, b) == -balance(b, a) — one side is the negative of the other.
  2. Sum across all users = 0 — money doesn't appear or disappear.
  3. Every split sums to the full expense amount — you didn't lose or invent rupees while dividing it up.

Each is enforceable in ONE place. Name them.

The extension question

An interviewer will ask "how would you add a settle-up feature that minimizes transactions?" That's not in MVP scope, but your design.md's Trade-offs section can note whether your representation makes that easy (yes, if you keep raw pairwise balances; harder, if you only stored net amounts).

Your task

Model an expense-splitting app (like Splitwise). Track who owes whom.

The problem

Users pay for shared expenses. The system tracks the running balance between every pair of users.

Users:

  • add_user(name) — register a user
  • Duplicate names raise ValueError.

Expenses:

  • add_expense(payer, amount, participants, split_type, shares=None)
  • payer — the user who paid (str)
  • amount — total amount paid (int, rupees)
  • participants — list of users the expense is shared with (the payer is included if they used the item)
  • split_type — one of:
    • "equal" — split evenly across participants
    • "exact"shares is a dict {user: amount}; must sum to amount
    • "percent"shares is {user: percent}; must sum to 100

For each expense, the payer is owed the participants' shares (minus their own share if they're a participant).

Queries:

  • balance(u1, u2) — how much does u1 owe u2 (positive) or is owed (negative)? int.
  • net_balance(user) — total across all pairs; positive means net-owed-TO, negative means net-owes.

Errors:

  • Unknown user in payer/participants → ValueError.
  • Shares don't sum correctly → ValueError.
  • split_type == "exact" or "percent" with no sharesValueError.

Assumption: use integer rupees. For unequal splits, we don't worry about rounding — the tests only give combinations that divide evenly.

Requirement-gathering questions

  1. Groups? — Not for MVP. Every expense declares its own participants.
  2. Settle-up / minimize transactions? — Not for MVP. We track raw pairwise balances. Simplification is a follow-up.
  3. Currency conversion? — No, integer rupees.
  4. Multiple currencies? — No.
  5. Concurrency? — Single-threaded MVP.

Design goals

  • Adding a new split type (e.g., "by_ratio") should be a manageable change.
  • Balance queries should be O(1) after all expenses are recorded — don't re-walk the whole expense log per query.
  • Invariants: total system balance always sums to zero across all users; balance(a, b) == -balance(b, a).

Your task

Public API:

from main import Splitwise

s = Splitwise()
s.add_user("alice")
s.add_user("bob")
s.add_user("carol")

s.add_expense(
    payer="alice", amount=300,
    participants=["alice", "bob", "carol"],
    split_type="equal",
)
# alice paid 300, each owes 100. alice's share is her own → skip.
# bob owes alice 100. carol owes alice 100.

s.balance("bob", "alice")     # 100 (bob owes alice)
s.balance("alice", "bob")     # -100
s.net_balance("alice")        # 200 (owed to her)
s.net_balance("bob")          # -100 (she owes)

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
06-splitwise-starter