Design Splitwise
Groups, expenses, and split strategies (equal/exact/percent) with a balance-simplification engine.
Problem Statement
Design the object model for a Splitwise-style expense sharing system. Users can join groups, one member can record an expense paid on behalf of others, and the system maintains net balances so everyone can see who owes whom.
The core design must support equal, exact amount, and percentage splits, update balances whenever an expense is committed, and simplify debts into the smallest practical list of settle-up payments.
Business context
Splitwise is a favorite intermediate LLD problem at product and fintech companies because it looks like simple bookkeeping but quickly exposes design quality. Good candidates separate split calculation from expense orchestration, keep money arithmetic precise, preserve the zero-sum balance invariant, and describe how settlements are derived from balances rather than stored as a second source of truth.
Functional Requirements
Create users with stable identity, display name, and email.
Create a group and add users as members.
Record an expense with payer, amount, and a list of participant splits.
Support equal, exact, and percentage split strategies.
Validate that all split shares add up exactly to the expense amount.
Maintain a net balance sheet for the group after every committed expense.
Show a member's current net balance.
Generate simplified settle-up payments from current balances.
Return immutable snapshots for history, members, and balances so callers cannot corrupt state.
Non-Functional Requirements
Money correctness
Use BigDecimal with two-decimal rounding. Never model balances with floating point numbers.
Consistency
After every balance update, the sum of all net balances must be zero. Any non-zero total is a domain bug.
Extensibility
Adding a new split mode such as shares, weights, or caps should mean adding a Split subclass, not editing expense validation.
Thread safety at aggregate boundaries
Group, ExpenseService, and BalanceSheet synchronize mutations so member checks, expense writes, and balance updates cannot interleave incorrectly.
Defensive reads
Membership, history, and balance snapshots are copied before exposure to keep the in-memory model encapsulated.
Requirement Clarification
QShould users belong to multiple groups?
The core model designs one group at a time. A production service can keep many Group instances and route requests by group id.
QDoes the payer have to be included in the splits?
No. The reference model supports both cases. The payer is credited for the full paid amount, and every listed split user is debited for their share.
QDo we store every pairwise debt edge?
No. Store only net balances per user. Pairwise payments are derived by SettlementService when the user asks to settle up.
QHow exact must percentage splits be?
The base implementation rounds each participant share to two decimals and then validates the sum equals the expense amount. In production, remainder allocation should be explicit.
QIs payment execution in scope?
No. Payment is a settlement instruction. Integrating banks or wallets is a boundary adapter and appears as a follow-up.
UML Class Diagram
Sequence Diagram
Entity Identification
User
Identity value object for a member. Equality and hashing use id, which keeps balance maps stable even when display details change.
Group
Aggregate for membership and the group's BalanceSheet. It exposes synchronized membership operations and owns the single ledger for the group.
Split
Abstract split strategy. It binds a participant user and defines shareOf and validate so each concrete split mode owns its own math.
EqualSplit
Concrete strategy that divides the total amount evenly across the number of split entries, rounding to two decimals.
ExactSplit
Concrete strategy that stores a fixed owed amount for the participant and rejects negative or over-large shares.
PercentSplit
Concrete strategy that stores a percentage and derives the participant amount from the expense total.
Expense
Immutable transaction record. It validates positive amount, non-empty splits, each split's rule, and the final sum before the ledger can observe it.
BalanceSheet
Synchronized net-balance ledger. Applying an expense credits the payer, debits participants, removes zero balances, and enforces total zero.
ExpenseService
Application service and factory method boundary. It validates membership, creates Expense ids, applies the expense to the sheet, stores history, and starts settlement.
SettlementService
Simplification algorithm. It converts positive balances into creditors, negative balances into debtors, and greedily emits payments between the largest remaining sides.
Payment
Settle-up instruction from one user to another for a precise amount. It is derived from balances and not stored as primary ledger truth.
Design Patterns Used
Split is the strategy abstraction. EqualSplit, ExactSplit, and PercentSplit vary share calculation and validation while Expense treats them uniformly.
BalanceSheet is the ledger observer of committed expenses. The reference implementation wires the notification synchronously through ExpenseService.addExpense with apply(expense), keeping balance mutation outside Expense itself.
ExpenseService.addExpense is the creation boundary for valid Expense objects: it checks membership, generates the id, constructs the expense, applies it, and returns the committed aggregate.
Step-by-Step Design
1Start with stable user identity and group membership
User equality is based on id, not email or display name. Group keeps members in a set and synchronizes reads and writes so membership checks are consistent during expense creation.
2Make split modes pluggable strategies
Put the variable math behind Split.shareOf and Split.validate. Expense can validate any split list without switch statements.
public abstract class Split { private final User user; public abstract BigDecimal shareOf(BigDecimal totalAmount, int splitCount); public abstract void validate(BigDecimal totalAmount); }3Validate each expense before touching balances
Expense is immutable and validates that the amount is positive, the split list is non-empty, and all calculated shares sum to the paid amount.
BigDecimal total = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); for (Split split : splits) { split.validate(amount); total = total.add(split.shareOf(amount, splits.size())); } if (total.compareTo(amount) != 0) { throw new IllegalArgumentException(); }4Use the service as the factory and transaction boundary
ExpenseService.addExpense checks that payer and participants are group members, creates a UUID-backed Expense, applies it to the ledger, appends history, and returns the committed object.
public synchronized Expense addExpense(User payer, BigDecimal amount, List<Split> splits) { ensureMember(payer); for (Split split : splits) ensureMember(split.getUser()); Expense expense = new Expense(UUID.randomUUID().toString(), payer, amount, splits); group.getBalanceSheet().apply(expense); expenses.add(expense); return expense; }5Update net balances as an observed side effect
BalanceSheet.apply credits the payer for the full amount and debits every split user for their share. Removing zero entries keeps snapshots compact, and the total-zero assertion catches drift.
6Derive settle-up payments from balances
SettlementService does not mutate the ledger. It separates creditors and debtors into priority queues, matches the largest credit with the largest debt, emits a Payment, and repeats until one side is empty.
7Keep read APIs defensive
getMembers, history, and snapshot return unmodifiable copies. This is important in LLD interviews because it proves callers cannot bypass domain methods and edit internal state.
Complete Java Implementation
Explanation of Every Class
User
Final identity class with id, displayName, and email. Equality and hash code depend only on id, which is exactly what a ledger map needs.
Group
Owns a synchronized LinkedHashSet of members and a single BalanceSheet. It gives the service a membership guard without exposing mutable internal state.
Split
Abstract base for split strategies. It stores the participant User, declares shareOf and validate, and provides money for two-decimal rounding.
EqualSplit
Concrete split strategy that divides the total amount by split count using two-decimal HALF_UP rounding. It only needs to ensure the expense amount is positive.
ExactSplit
Concrete split strategy with a fixed amount. It rejects negative shares and shares larger than the total expense.
PercentSplit
Concrete split strategy with a percent field. It validates the percentage range and converts it to money by multiplying the expense amount.
Expense
Immutable expense record with generated id, payer, rounded amount, copied splits, and timestamp. Construction fails unless split shares sum exactly to the amount.
BalanceSheet
Synchronized ledger of User to net BigDecimal. apply credits the payer, debits participants, removes zero balances, and asserts that the ledger remains zero-sum.
ExpenseService
Application service for a single group. It validates membership, factory-creates expenses with UUIDs, updates the balance sheet, stores history, answers balances, and delegates settlement.
SettlementService
Stateless debt simplifier. It builds creditor and debtor priority queues from a snapshot and repeatedly emits the minimum payment between the largest remaining amounts.
BalanceNode
Private helper inside SettlementService that pairs a User with an absolute amount for priority queue ordering.
Payment
Package-private settlement instruction with from, to, and amount. describe renders a human-readable instruction without exposing ledger mutation.
Dry Run
Sample input
Group members: Asha, Ben, Chen. Expense 1: Asha pays 300.00 split equally across all three. Expense 2: Ben pays 120.00 split exactly as Asha 50.00 and Chen 70.00.
| Step | Action | Asha | Ben | Chen | Output |
|---|---|---|---|---|---|
| 1 | Add members | 0.00 | 0.00 | 0.00 | All balances start at zero |
| 2 | Asha pays 300.00 equally | +200.00 | -100.00 | -100.00 | Asha credited 300.00 and debited 100.00 |
| 3 | Ben pays 120.00 exact | +150.00 | +20.00 | -170.00 | Asha debited 50.00, Chen debited 70.00 |
| 4 | settleUp() | +150.00 | +20.00 | -170.00 | Chen -> Asha 150.00; Chen -> Ben 20.00 |
The balance sheet stores net positions, not pairwise edges. Asha and Ben are creditors after both expenses; Chen is the only debtor, so settlement emits two payments.
Complexity Analysis
| Operation | Time | Space | Note |
|---|---|---|---|
| addExpense | O(S) | O(S) | S split entries. Membership checks are set lookups, validation iterates splits, and the expense stores a copied split list. |
| balanceOf | O(1) | O(1) | Hash map lookup in the balance sheet. |
| history | O(E) | O(E) | Returns a defensive copy of E expenses. |
| snapshot | O(U) | O(U) | Copies balances for U users with non-zero positions. |
| settleUp | O(U log U) | O(U) | Builds debtor and creditor priority queues and emits at most O(U) payments. |
For interview scale, the hot path is addExpense, linear in participants on that expense. settleUp is usually less frequent and depends on users with non-zero balances, not total expenses recorded.
Extensibility
New split strategy
Add a new Split subclass such as ShareSplit, WeightSplit, or CappedSplit. Expense and BalanceSheet keep calling shareOf and validate.
Multi-group application
Put Group instances behind a repository keyed by group id. ExpenseService can become request-scoped or receive the group for each command.
Real payment execution
Keep Payment as a domain instruction and add a PaymentProcessor adapter for wallets, UPI, cards, or bank transfers.
Expense categories and attachments
Add optional fields to Expense or a side object for category, notes, and receipt URLs without changing split math or settlement.
Notifications
Generalize the synchronous balance update into an observer list so expense-created events can update balances, send push notifications, and write audit logs.
Alternative Designs
Pairwise debt matrix
Store what each user owes every other user directly, updating pairwise edges on every expense.
Tradeoffs
Makes some user-to-user views fast, but creates O(U squared) state and makes simplification more complex. Net balances are a cleaner source of truth.
Event-sourced ledger
Append immutable expense events and recompute balances from the event log or maintain projections.
Tradeoffs
Excellent auditability and replay, but more infrastructure and eventual consistency. For an LLD interview, an in-memory balance sheet is clearer.
Explicit observer interface
Define an ExpenseObserver interface and let BalanceSheet, notification senders, and audit writers subscribe to committed expenses.
Tradeoffs
More extensible than a direct apply call, but adds ceremony. The reference keeps the observer relationship synchronous and simple.
Stored settlements
Persist generated Payment instructions as settlement sessions.
Tradeoffs
Useful for payment workflows, but dangerous if treated as truth. Balances must remain the canonical ledger until payments are actually recorded.
Common Mistakes
- ×
Using double or float for money and then fighting rounding bugs.
- ×
Keeping pairwise debts as the primary source of truth when net balances are enough for settlement.
- ×
Putting equal, exact, and percentage logic in ExpenseService with conditionals instead of a Split strategy hierarchy.
- ×
Forgetting to validate that split shares sum to the expense amount.
- ×
Allowing non-members to appear in an expense because only the payer was checked.
- ×
Updating history before balance application succeeds, leaving a committed expense without ledger changes.
- ×
Returning mutable lists or maps from Group, ExpenseService, or BalanceSheet.
- ×
Treating simplified Payment instructions as already-settled money movement.
Follow-up Interview Questions
QHow would you record that Chen actually paid Asha 150.00?
Model settlement as another ledger event: payer Chen, receiver Asha, amount 150.00. Apply it by debiting Asha's credit and reducing Chen's debt, then keep an audit record.
QHow do you support recurring expenses like monthly rent?
Add a scheduler that creates normal Expense objects from a recurrence rule. The ledger should not care whether an expense was manual or scheduled.
QHow would you make percentage splits robust when rounding leaves one cent?
Choose a deterministic remainder policy, such as assigning leftover cents to the payer or to participants sorted by id, and test it explicitly.
QCan this design support individual friendships outside groups?
Yes. Represent a one-to-one group or introduce a Ledger abstraction shared by groups and friendships. The split, expense, and settlement logic remains reusable.
QWhat changes for millions of users?
Persist users, expenses, and balances; shard by group id; make addExpense transactional; publish expense-created events for async projections and notifications.
Production Considerations
Transactional persistence
Store expense history and balance deltas in one database transaction. A committed expense without its balance update is an accounting incident.
Audit trail
Keep immutable expense and settlement events, including who created them and when. Financial products need explainability and rollback paths.
Idempotency
Accept a client request id for addExpense so retries after network failures do not create duplicate expenses.
Currency and locale
Add a currency field and use currency-specific scales. Do not mix balances across currencies without an explicit conversion event.
Notifications and projections
Publish an expense-created event after commit. Balance projections, push notifications, email, and analytics can consume it independently.
Privacy and access control
Verify group membership for every read and write. Expense history and balances are sensitive financial data.
What Interviewers Look For
Did the candidate identify Split as the Strategy seam and avoid switch-heavy split calculation?
Did they protect the zero-sum balance invariant after every expense?
Did they separate net balances from derived settle-up payments?
Did they use precise money types and discuss rounding edge cases?
Did they keep ExpenseService as the consistency boundary for membership, creation, ledger update, and history?
Can they explain the greedy settlement algorithm and its complexity?
Quiz
0/5 answered
1.Why is **Split** an abstract class with **shareOf** and **validate**?
2.What invariant does **BalanceSheet.apply** verify after updating balances?
3.Why does **settleUp** use priority queues of creditors and debtors?
4.Which class acts as the factory method boundary for creating committed expenses?
5.Why are **history** and **snapshot** returned as defensive copies?
Practice Variants
Add share-based splits
BeginnerImplement ShareSplit where each participant has N shares and pays amount times their share divided by total shares. Keep Expense unchanged.
Record actual settlements
IntermediateAdd a command that records a real payment between two users and applies it to the balance sheet as a ledger event.
Multi-currency groups
AdvancedAllow a group to track balances per currency and reject settlement across currencies unless an explicit conversion event exists.
Expense observers
IntermediateIntroduce an ExpenseObserver interface so balance updates, notifications, analytics, and audit logging subscribe to the same committed expense event.
Flashcards
Cheat Sheet
Entities: User, Group, Split hierarchy, Expense, BalanceSheet, ExpenseService, SettlementService, Payment.
Patterns: Strategy for split calculation; Observer-style balance update on committed expense; Factory Method through ExpenseService.addExpense.
Core flow: addExpense = validate members -> create Expense -> validate split totals -> apply BalanceSheet -> append history.
Balance rule: payer gets credited by the paid amount; every split user is debited by their computed share; all balances must sum to zero.
Settlement: derive payments from net balances using creditor and debtor priority queues. Do not store pairwise debts as primary truth.
Complexity: addExpense O(S), balanceOf O(1), snapshot O(U), settleUp O(U log U).
Interview hooks: money precision, rounding remainders, idempotency, settlement recording, multi-currency, and observer-based notifications.
References
- BookHead First Design Patterns (Strategy and Observer) — Freeman & Robson
- BookEffective Java — Item 17 (minimize mutability) and Item 60 (avoid float and double if exact answers are required) — Joshua Bloch
- DocsRefactoring Guru — Strategy Pattern
- DocsRefactoring Guru — Observer Pattern