Design Splitwise
OOP design for an expense-splitting application covering group management, multiple split strategies (equal, exact, percentage), balance simplification with debt graph minimization, and settlement tracking.
The Problem
You and five friends just finished a week-long trip. One person paid for all the hotels, another covered every dinner, a third rented the car. Now it is 11 p.m. at the airport and everyone is pulling out calculators, arguing over who owes whom and how much. Someone suggests "let's just each Venmo each other," which spawns 15 separate transfers when only 4 were necessary. The group chat stays angry for a week.
An expense-splitting application fixes this. It records who paid what, splits costs across participants using different strategies (equal, exact amounts, percentages, share-based), tracks running balances between every pair of users, and simplifies the resulting debt graph so the minimum number of settlements clear all debts. The hard parts? Supporting multiple split strategies without a giant if-else chain, simplifying complex multi-person debts into minimal transactions, and managing both group and non-group expenses cleanly.
Design the core classes for an expense-splitting system that supports group management, pluggable split strategies, per-pair balance tracking, debt graph simplification, and settlement recording.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to turn the vague prompt into a concrete specification. Cover four areas: core actions, error handling, boundaries, and future extensions.
You: "Can an expense involve people who are not in the same group? Or are all expenses group-scoped?"
Interviewer: "Both. Users can split an expense with any set of users, group or not. Groups are just a convenience for recurring splits among the same people."
Good. Groups are optional containers, not gatekeepers. An expense links to a list of participants regardless of group membership.
You: "Who pays the expense? Always one person, or can multiple people split the payment side too?"
Interviewer: "Support both single-payer and multi-payer. For multi-payer, each payer specifies how much they contributed."
Multi-payer adds a second dimension: who paid how much AND who owes how much. Both sides need split logic.
You: "What split strategies should we support?"
Interviewer: "Four: equal (total divided by N), exact (each person's amount specified), percentage (each person's percentage specified), and share-based (proportional to a weight or number of shares)."
Four strategies with the same interface but different math. Classic Strategy pattern.
You: "For equal splits, what about rounding? If $100 splits three ways, someone gets the extra cent."
Interviewer: "Assign the remainder cents to the first participants in the list. The splits must always sum to the exact total."
That constraint is subtle. The split strategy must guarantee that the individual amounts sum precisely to the total, never a cent more or less.
You: "How should the system track who owes whom?"
Interviewer: "Maintain a net balance for every pair of users. If Alice paid $60 for Bob, and Bob paid $40 for Alice elsewhere, the net is Alice is owed $20 by Bob."
Pairwise net balances. I will store a signed amount per user-pair: positive means one direction, negative means the other.
You: "Should the system simplify debts? For example, if A owes B $10, B owes C $10, can it simplify to A owes C $10?"
Interviewer: "Yes. Implement a debt simplification algorithm that minimizes the number of transactions needed to settle all debts."
This is the hardest part of the problem. Debt simplification is essentially a graph problem where we minimize edge count while preserving net flow.
You: "When a user settles a debt, do we record that as a special transaction?"
Interviewer: "Yes. A settlement is a recorded payment from one user to another that reduces their balance. It's not an expense, it's a clearance."
Settlements are first-class entities, distinct from expenses. They modify balances directly.
You: "Should we handle multiple currencies or just one?"
Interviewer: "Single currency for now, but design it so multi-currency could be added later."
Perfect. You have now clarified scope and ruled out unnecessary complexity.
Final Requirements
Functional Requirements:
- Users can create expenses with a description, total amount, payer(s), and participants
- Four split strategies: equal, exact, percentage, and share-based
- Users can create groups and add/remove members
- The system tracks net balances between every pair of users
- Debt simplification minimizes the number of transactions to settle all debts
- Users can record settlements that reduce outstanding balances
Non-Functional Requirements:
- Thread safety for concurrent expense creation and balance updates
- Extensibility for new split strategies without modifying existing code
- Splits must always sum to exactly the total amount (cent-precise)
Out of Scope:
- UI rendering and notifications
- Persistence / database
- Multi-currency support (designed for, not implemented)
- Payment gateway integration
Example Inputs and Outputs
Scenario 1: Equal split among three users
- Input: Alice pays $90 for dinner. Split equally among Alice, Bob, and Charlie.
- Expected: Bob owes Alice $30. Charlie owes Alice $30. Alice's net: owed $60.
- Why: validates the basic equal split and balance tracking.
Scenario 2: Percentage split
- Input: Bob pays $200 for a hotel. Split: Alice 50% ($100), Bob 25% ($50), Charlie 25% ($50).
- Expected: Alice owes Bob $100. Charlie owes Bob $50. Bob's own share ($50) cancels out.
- Why: validates percentage split where the payer is also a participant.
Scenario 3: Debt simplification
- Input: Three existing debts: Alice owes Bob $40, Bob owes Charlie $30, Alice owes Charlie $10.
- Expected simplified: Alice pays Bob $10, Alice pays Charlie $40. Two transactions instead of three.
- Why: validates the simplification algorithm reduces transaction count by netting balances.
Scenario 4: Settlement
- Input: Alice owes Bob $30 (from prior expenses). Alice records a $30 settlement to Bob.
- Expected: Alice-Bob balance becomes $0. The settlement is recorded with a timestamp.
- Why: validates that settlements correctly reduce balances.
Try It Yourself
Try it yourself
Before reading the solution, spend 20 minutes sketching your own class diagram. Focus on how you would represent splits (hint: polymorphism), how balances are tracked between pairs of users, and how you would simplify a multi-person debt graph. Compare your approach with the walkthrough below.
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this problem? Scan the requirements for nouns and group them by responsibility. An expense-splitting system has more moving parts than most LLD problems because it blends financial logic (splits, balances) with social structure (groups, users).
A common mistake is shoving everything into a single ExpenseManager god class. Good design gives each class a single, clear job. Another trap: merging Expense and Split into one class. They have different lifecycles; an expense is created once, but its splits are computed by a strategy and may vary depending on which strategy is used.
| Entity | Responsibility | Key attributes |
|---|---|---|
| User | Identity holder. Stores name, email, unique ID. No business logic. | id, name, email |
| Group | Container for a recurring set of users. Provides convenience, not logic. | id, name, members |
| Expense | Records what was paid, by whom, for whom, and how to split it. | id, description, amount, payer, participants, splitType |
| Split | Value object holding one participant's computed share of an expense. | userId, amount |
| SplitStrategy | Computes how an expense total is divided among participants. | (interface with one method) |
| BalanceSheet | Tracks net balances between every pair of users. The financial ledger. | balanceMap |
| Settlement | Records a payment from one user to another that clears debt. | id, payer, payee, amount, timestamp |
| ExpenseManager | Orchestrator. Creates expenses, delegates splits, updates balances. | users, groups, balanceSheet, expenses |
Notice that Split is a value object (a record in Java), not a full entity. It has no identity of its own; it exists only as part of an expense. SplitStrategy is an interface because the four split types share the same contract but have completely different math. Keeping BalanceSheet separate from ExpenseManager means you can test balance logic independently.
Step 2: Define Relationships and Class Design
ExpenseManager (the orchestrator)
ExpenseManager is the entry point for all operations. It coordinates user registration, group management, expense creation, and settlement recording.
Deriving state from requirements:
| Requirement | What ExpenseManager must track |
|---|---|
| "Users can create expenses" | A list of all expenses, the payer, participants |
| "Users can create groups" | A registry of groups and their members |
| "Track net balances" | A BalanceSheet that holds pairwise balances |
| "Record settlements" | The BalanceSheet to update when settlements occur |
This gives us the state: a user registry, a group registry, the expense list, and a single BalanceSheet instance.
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "Create expenses with split strategies" | addExpense(request, strategy) |
| "Record settlements" | addSettlement(payerId, payeeId, amount) |
| "View balances for a user" | getBalancesForUser(userId) |
| "Simplify debts" | simplifyDebts() |
BalanceSheet (the financial ledger)
BalanceSheet tracks the net balance between every pair of users. It does not know about expenses or groups; it only understands "user A paid X for user B."
Deriving state from requirements:
| Requirement | What BalanceSheet must track |
|---|---|
| "Net balance for every pair" | A Map from (userA, userB) to signed BigDecimal |
| "Simplify debts" | Must be able to compute all non-zero balances and reduce them |
I store balances as Map<String, Map<String, BigDecimal>>. A positive value at balances[A][B] means B owes A that amount. When B also pays for A, we subtract. The net result is always one direction.
Key Relationship Decisions
Expense owns Splits (composition): Splits do not exist independently. When an expense is deleted, its splits vanish too. This is composition, not association.
ExpenseManager owns BalanceSheet (composition): There is exactly one balance ledger per system. It has no meaning outside the manager.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.