Skip to content

Splitwise (LLD)#

Problem statement (interviewer prompt)

Design the class model for Splitwise: users, groups, expenses with multiple splits (equal, exact, percent), and a balance ledger that tracks who owes whom. Add an option to simplify N×N debts into the minimum transfers.

classDiagram
  class User {
    +id, name, email
  }
  class Group {
    +id, name
    -members : Set~User~
    +addMember(u)
    +addExpense(e)
  }
  class Expense {
    +id, description, amount, paidBy : User
    -splits : List~Split~
  }
  class Split {
    <<abstract>>
    +user : User
    +amount(total) double
  }
  class EqualSplit
  class ExactSplit { +exact : double }
  class PercentSplit { +percent : double }
  class BalanceSheet {
    -balances : Map~User, Map~User, double~~
    +record(expense)
    +balanceOf(u) Map
    +simplify() List~Transfer~
  }
  Split <|-- EqualSplit
  Split <|-- ExactSplit
  Split <|-- PercentSplit
  Expense *-- "many" Split
  Group *-- "many" User
  Group *-- "many" Expense
  BalanceSheet ..> Expense

Strategy pattern for split types. BalanceSheet maintains the running ledger; simplify reduces N×N edges to a minimum-transfer set using a heap.

A canonical OO interview problem: small surface area, rich modelling decisions, clear extension points.

Domain model#

classDiagram
  class User {
    +id, name, email
  }
  class Group {
    +id, name
    -members : Set~User~
    -expenses : List~Expense~
    +addMember(u)
    +addExpense(e)
  }
  class Expense {
    +id, description
    +amount : double
    +paidBy : User
    +createdAt : Instant
    -splits : List~Split~
    +validate() boolean
  }
  class Split {
    <<abstract>>
    +user : User
    +amountFor(total) double
  }
  class EqualSplit {
    +amountFor(total) total/N
  }
  class ExactSplit {
    +exact : double
    +amountFor(total) exact
  }
  class PercentSplit {
    +percent : double
    +amountFor(total) total*percent/100
  }
  class BalanceSheet {
    -balances : Map~User, Map~User, double~~
    +record(expense)
    +balanceBetween(a, b) double
    +balanceOf(u) Map~User, double~
    +simplify() List~Transfer~
  }
  class Transfer {
    +from, to : User
    +amount : double
  }
  Split <|-- EqualSplit
  Split <|-- ExactSplit
  Split <|-- PercentSplit
  Expense *-- "many" Split
  Group *-- "many" User
  Group *-- "many" Expense
  BalanceSheet ..> Expense
  BalanceSheet ..> Transfer

Recording an expense#

def record(self, e: Expense):
    assert sum(s.amount_for(e.amount) for s in e.splits) == e.amount
    for split in e.splits:
        if split.user == e.paid_by:
            continue
        owed = split.amount_for(e.amount)
        self.balances[split.user][e.paid_by] += owed
        self.balances[e.paid_by][split.user] -= owed

Each user has a map of how much they owe (positive) or are owed (negative) per other user.

Split strategies#

Type When
EqualSplit Default; total / N
ExactSplit "Alice paid $50, owes $20 only"
PercentSplit "Bob owns 40% of the rent"
ShareSplit Weighted shares (1, 2, 1)

Validate at construction: sums must equal the total within a cent tolerance.

Simplify debts#

flowchart TB
  Net[Compute net balance per user] --> Heap[Max heap of creditors, min heap of debtors]
  Heap --> Match[Pop biggest debtor and biggest creditor]
  Match --> Transfer[Create transfer = min of two, reduce balances]
  Transfer --> Repeat[Repeat until both heaps empty]

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class Net,Heap,Match,Transfer,Repeat service;

Greedy approximation - finds at most N-1 transfers (vs up to N(N-1)/2 raw). Exact minimum is NP-hard.

Concurrency#

  • BalanceSheet should be transactional: record(expense) atomic across both users' maps.
  • Persist via outbox if syncing to other services.
  • For multi-user concurrent writes, lock per-group.

Audit log / history#

Every record/settle produces an immutable LedgerEntry; balances are the materialised view of summing entries. Enables time-travel ("what did we owe last month?").

Extensions#

  • Multi-currency (each Expense has Currency; ledger keeps separate per-currency balances).
  • Recurring expenses (cron expression).
  • Comment threads on expenses.
  • Receipt OCR.
  • Payment integration (settle through Stripe, UPI).

Where Splitwise LLD fits#

flowchart TB
  SW((Splitwise<br/>LLD))
  BPL[Behavioral patterns<br/>Strategy for splits]
  OOP[OOP pillars<br/>inheritance for splits]
  SWH[Splitwise HLD<br/>system-design sibling]
  DE[Domain events + aggregates<br/>LedgerEntry as event]
  BPL --> SW
  OOP --> SW
  SW -. complement .- SWH
  SW --> DE

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class SW service;
    class BPL,OOP,SWH,DE datastore;

Glossary & fundamentals#

Tag Concept What it is Page
LLD Behavioral patterns Strategy for splits behavioral-patterns
LLD OOP pillars inheritance for Split types oop-pillars
HLD Splitwise (HLD) the system-design sibling splitwise
LLD Domain events and aggregates LedgerEntry as event domain-events-aggregates

Quick reference#

Classes#

  • User, Group
  • Expense (paidBy, amount, splits)
  • Split (Equal / Exact / Percent / Share)
  • BalanceSheet (per-user owed-map)
  • Transfer (from, to, amount)

Recording#

For each split: increment owed (debtor → creditor), decrement reverse.

Validation#

Sum of split amounts == expense total within cent tolerance.

Simplify (greedy)#

  1. Net balance per user
  2. Max-heap of creditors, min-heap of debtors
  3. Pop largest of each; settle min; reinsert remainder
  4. ≤ N-1 transfers (vs raw N(N-1)/2)

Concurrency#

Per-group lock around record/settle.

Extensions#

  • Multi-currency
  • Recurring expenses
  • Payment integration (Stripe, UPI)
  • Audit log via LedgerEntry events
  • Receipt OCR

Refs#

  • LeetCode #465 (similar simplify variant)
  • Splitwise engineering blog

FAQ#

How do you model an expense in Splitwise?#

An Expense has a payer, total amount, and a list of Split objects (one per participant). Each Split records the share, so the balance ledger updates atomically.

Which design pattern fits the split types?#

Strategy: an abstract SplitStrategy with EqualSplit, ExactSplit, and PercentSplit implementations. The Expense calls strategy.compute(total, participants) to get the per-user share.

How is the balance ledger maintained?#

A two-level map balance[a][b] holds how much a owes b. Adding an Expense increments payer credits and participant debts symmetrically so totals always net to zero.

How does the simplify-debts algorithm work?#

Compute each user's net balance, then greedily match the largest creditor with the largest debtor until all balances are zero, producing the minimum transfer count.

How do you support group expenses?#

Expense optionally references a Group; reports filter by group membership. The same balance ledger is reused, with views that aggregate only intra-group transactions.

Further reading#

Video walkthrough

LLD of Splitwise: Expense Sharing App Design : via Concept && Coding