Splitwise Low Level Design is one of the most intellectually rich LLD interview problems. It requires the Strategy pattern for multiple split types, a balance tracking system, and a graph minimization algorithm to simplify debts. It is frequently asked at fintech companies like Razorpay, CRED, PhonePe, and Slice. This guide covers the complete Splitwise LLD solution with Java code.
Why Interviewers Ask Splitwise LLD
Splitwise tests multiple skills simultaneously — entity design, multiple design patterns, and a non-trivial algorithm. Interviewers use it to see:
- Can you model financial relationships cleanly — who paid, who owes what?
- Do you use Strategy pattern for extensible split types instead of if-else blocks?
- Can you implement the debt simplification algorithm (graph reduction)?
- Do you separate concerns — ExpenseService vs BalanceService vs SettlementService?
Functional Requirements
- Users can create groups and add members
- A user can add an expense — who paid, how much, who participated
- Support three split types: Equal, Exact amount, Percentage
- Show each user's net balance within a group
- Show who owes whom and how much
- Simplify debts — minimize the number of transactions needed to settle all balances
- Record a settlement when one user pays another
Non-Functional Requirements
- Split percentages must sum to 100; exact amounts must sum to the total expense
- Balance updates must be atomic — concurrent expense additions must not corrupt balances
- New split types (e.g., shares-based) should be addable without changing existing code
Core Entities — Splitwise LLD Class Design
- User — id, name, email
- Group — id, name, list of members, list of expenses
- Expense — id, description, totalAmount, paidBy (User), split type, participants
- ExpenseSplit — userId, amount owed for a specific expense
- Balance — net amount between two users (positive = owed to you)
- Settlement — from userId, to userId, amount, timestamp
- SplitStrategy — interface; EqualSplit, ExactSplit, PercentageSplit implement it
Text-Based Class Diagram
User +-- id, name, email Group +-- id, name +-- members: List<User> +-- expenses: List<Expense> Expense +-- id, description, totalAmount +-- paidBy: User +-- splitStrategy: SplitStrategy +-- participants: List<Participant> Participant +-- user: User +-- exactAmount: Double (for ExactSplit) +-- percentage: Double (for PercentageSplit) SplitStrategy (interface) +-- calculateShares(amount, participants): Map<String, Double> EqualSplit implements SplitStrategy ExactSplit implements SplitStrategy PercentageSplit implements SplitStrategy Balance +-- fromUserId, toUserId +-- amount: double Settlement +-- from: User, to: User +-- amount: double, timestamp
Strategy Pattern for Split Types — Java
public interface SplitStrategy {
Map<String, Double> calculateShares(double amount, List<Participant> participants);
}
public class EqualSplit implements SplitStrategy {
@Override
public Map<String, Double> calculateShares(double amount, List<Participant> participants) {
double share = amount / participants.size();
Map<String, Double> shares = new LinkedHashMap<>();
participants.forEach(p -> shares.put(p.getUserId(), share));
return shares;
}
}
public class ExactSplit implements SplitStrategy {
@Override
public Map<String, Double> calculateShares(double amount, List<Participant> participants) {
double total = participants.stream().mapToDouble(Participant::getExactAmount).sum();
if (Math.abs(total - amount) > 0.01)
throw new IllegalArgumentException("Exact amounts must sum to total: " + amount);
Map<String, Double> shares = new LinkedHashMap<>();
participants.forEach(p -> shares.put(p.getUserId(), p.getExactAmount()));
return shares;
}
}
public class PercentageSplit implements SplitStrategy {
@Override
public Map<String, Double> calculateShares(double amount, List<Participant> participants) {
double totalPct = participants.stream().mapToDouble(Participant::getPercentage).sum();
if (Math.abs(totalPct - 100) > 0.01)
throw new IllegalArgumentException("Percentages must sum to 100");
Map<String, Double> shares = new LinkedHashMap<>();
participants.forEach(p -> shares.put(p.getUserId(), amount * p.getPercentage() / 100));
return shares;
}
}ExpenseService — Recording Expenses and Updating Balances
public class ExpenseService {
private final BalanceRepository balanceRepo;
public void addExpense(Expense expense) {
Map<String, Double> shares = expense.getSplitStrategy()
.calculateShares(expense.getTotalAmount(), expense.getParticipants());
shares.forEach((userId, share) -> {
if (userId.equals(expense.getPaidBy().getId())) return;
// userId owes paidBy 'share' rupees
balanceRepo.updateBalance(userId, expense.getPaidBy().getId(), share);
});
}
}Debt Simplification Algorithm
This is the hardest part — minimize the number of transactions needed to settle all debts. The algorithm computes a net balance for each user (positive = owed money, negative = owes money), then greedily matches the largest creditor with the largest debtor.
public List<Settlement> simplifyDebts(Group group) {
// Step 1: compute net balance per user
Map<String, Double> netBalance = new HashMap<>();
for (User member : group.getMembers()) {
double net = balanceRepo.getNetBalance(member.getId(), group.getId());
netBalance.put(member.getId(), net);
}
// Step 2: split into creditors and debtors
List<double[]> creditors = new ArrayList<>(); // [userId-hash, amount]
List<double[]> debtors = new ArrayList<>();
// Using indices for simplicity; in real code store userId
List<String> ids = new ArrayList<>(netBalance.keySet());
for (String id : ids) {
double bal = netBalance.get(id);
if (bal > 0.01) creditors.add(new double[]{ids.indexOf(id), bal});
else if (bal < -0.01) debtors.add(new double[]{ids.indexOf(id), -bal});
}
creditors.sort((a, b) -> Double.compare(b[1], a[1]));
debtors.sort((a, b) -> Double.compare(b[1], a[1]));
// Step 3: greedily settle
List<Settlement> settlements = new ArrayList<>();
int i = 0, j = 0;
while (i < creditors.size() && j < debtors.size()) {
double amount = Math.min(creditors.get(i)[1], debtors.get(j)[1]);
String credId = ids.get((int) creditors.get(i)[0]);
String debtId = ids.get((int) debtors.get(j)[0]);
settlements.add(new Settlement(debtId, credId, amount));
creditors.get(i)[1] -= amount;
debtors.get(j)[1] -= amount;
if (creditors.get(i)[1] < 0.01) i++;
if (debtors.get(j)[1] < 0.01) j++;
}
return settlements;
}Key Design Decisions
- Strategy over if-else for split types: If you write
if (type == EQUAL) ... else if (type == EXACT) ...inside Expense, adding a new split type means modifying Expense. With Strategy, it's a new class — Open/Closed Principle. - Participant model instead of just userIds: Exact and percentage splits need per-participant metadata. The Participant class carries userId + optional exactAmount + optional percentage cleanly.
- Separate BalanceRepository: Balances are derived state computed from expenses. Keeping them in a dedicated repository with atomic updates prevents race conditions when multiple expenses are added concurrently.
Common Follow-Up Questions
- "What if two users add expenses to the same group simultaneously?" — Use optimistic locking on balance records, or use a message queue to serialize expense additions per group.
- "How do you handle currency conversion for international groups?" — Add a currency field to Expense. BalanceService uses a CurrencyConverter (Strategy pattern again) to normalize all balances to a base currency before computing net amounts.
- "What is the time complexity of the debt simplification algorithm?" — O(n log n) for sorting creditors and debtors, O(n) for the greedy matching. Total O(n log n).
FAQ — Splitwise Low Level Design
What design pattern does Splitwise use?
The primary pattern is Strategy for split types (EqualSplit, ExactSplit, PercentageSplit). The Repository pattern is used for balance tracking, and theService layer pattern separates business logic (ExpenseService, BalanceService, SettlementService) from domain entities.
How does Splitwise simplify debts?
Compute the net balance for each user across all expenses. Users with positive balance are creditors; negative balance are debtors. Greedily match the largest debtor with the largest creditor, create a settlement for the minimum of the two amounts, and reduce both balances. Repeat until all balances are zero. This produces the minimum number of transactions.
What is the Splitwise LLD class diagram?
Core classes: User, Group (has many Users and Expenses), Expense (has paidBy User, SplitStrategy, and list of Participants), SplitStrategy interface (EqualSplit/ExactSplit/PercentageSplit), Balance (net amount between two users), Settlement (payment record).