Requirements
Requirements
Functional Requirements
- Users: register, create groups, add friends
- Groups: create group, add/remove members, settle all debts
- Expenses: add expense, specify split type, track who paid
- Balances: view who owes whom, view group total
- Settlements: record payments, mark debts as settled
- Notifications: notify when added to expense, when debt simplifies
Non-Functional Requirements
- Eventually consistent balances (not real-time critical)
- Audit trail for all transactions
- Support multiple currencies
- Mobile + Web clients
Core Entities
User
- userId, name, email, phone
- balances: Map<GroupId, Map<UserId, Amount>> // who owes whom
Group
- groupId, name, members[]
- expenses[]
- simplifedDebts[]
Expense
- expenseId, description
- amount, currency
- paidBy (userId)
- splitAmong[] (list of userIds)
- splitType (EQUAL, EXACT, PERCENTAGE, SHARES)
- splitDetails: Map<UserId, Amount>
- groupId (nullable - can be settle-up between 2 people)
- createdAt, createdBy
Settlement
- settlementId
- fromUser, toUser, amount
- status (PENDING, COMPLETED)
- createdAt
Split Types
| Type | Description | Example |
|---|---|---|
| EQUAL | Divide equally among all | $30 / 3 people = $10 each |
| EXACT | Each person pays specific amount | Alice $15, Bob $10, Carol $5 |
| PERCENTAGE | Each pays % of total | 50%, 30%, 20% |
| SHARES | Divide by shares | 2:1:1 ratio → 50%, 25%, 25% |
Expense Model
Expense Model
Balance Tracking
For each group, maintain a balance map: Map<UserId, Map<UserId, Amount>>
Group: Trip to NYC
Alice pays $120 → split equally among Alice, Bob, Carol
Balance updates:
Alice: {Bob: +$40, Carol: +$40}
Bob: {Alice: -$40}
Carol: {Alice: -$40}
Interpretation:
Alice is OWED $40 by Bob
Alice is OWED $40 by Carol
Bob OWES Alice $40
Carol OWES Alice $40
Adding an Expense
1. Create expense with split type and details
2. Calculate each person's share based on SplitStrategy
3. Update balances:
- For each person EXCEPT payer: add their share to payer's balance
- Payer's balance unchanged (they already paid)
4. Save expense to database
5. Notify all participants
Java Implementation
public class Expense {
private final String expenseId;
private final String description;
private final double amount;
private final String paidBy;
private final List<String> splitAmong;
private final SplitType splitType;
private final Map<String, Double> splitDetails; // userId → amount
private final String groupId;
private final LocalDateTime createdAt;
public Expense(String description, double amount, String paidBy,
List<String> splitAmong, SplitType splitType,
Map<String, Double> splitDetails, String groupId) {
this.expenseId = UUID.randomUUID().toString();
this.description = description;
this.amount = amount;
this.paidBy = paidBy;
this.splitAmong = splitAmong;
this.splitType = splitType;
this.splitDetails = splitDetails;
this.groupId = groupId;
this.createdAt = LocalDateTime.now();
}
}
public interface SplitStrategy {
Map<String, Double> calculateSplit(double amount, List<String> users);
}
public class EqualSplit implements SplitStrategy {
@Override
public Map<String, Double> calculateSplit(double amount, List<String> users) {
double share = amount / users.size();
return users.stream()
.collect(Collectors.toMap(u -> u, u -> share));
}
}
public class ExactSplit implements SplitStrategy {
@Override
public Map<String, Double> calculateSplit(double amount, List<String> users) {
// amounts provided directly via splitDetails in Expense
throw new UnsupportedOperationException("Use splitDetails from Expense constructor");
}
}
public class PercentageSplit implements SplitStrategy {
@Override
public Map<String, Double> calculateSplit(double amount, List<String> users) {
// percentages provided via splitDetails (e.g., {alice: 50, bob: 30, carol: 20})
throw new UnsupportedOperationException("Use splitDetails from Expense constructor");
}
}
public class SharesSplit implements SplitStrategy {
@Override
public Map<String, Double> calculateSplit(double amount, List<String> users) {
// shares provided via splitDetails (e.g., {alice: 2, bob: 1, carol: 1})
throw new UnsupportedOperationException("Use splitDetails from Expense constructor");
}
}
public class ExpenseService {
private final BalanceService balanceService;
private final NotificationService notificationService;
public void addExpense(Expense expense) {
Map<String, Double> splits = expense.getSplitDetails();
// Update balances: each person owes payer their share
for (Map.Entry<String, Double> entry : splits.entrySet()) {
String userId = entry.getKey();
Double share = entry.getValue();
if (!userId.equals(expense.getPaidBy())) {
balanceService.updateBalance(
expense.getGroupId(),
expense.getPaidBy(), // creditor
userId, // debtor
share // amount
);
}
}
// Notify all participants
for (String userId : splits.keySet()) {
notificationService.notifyExpenseAdded(userId, expense);
}
}
}
Debt Settlement
Debt Settlement
Problem: Minimize Transactions
Without simplification, if A owes B $50, B owes C $30, C owes A $20, you'd have 3 transactions. After simplification, only 1 transaction needed.
Algorithm: Net Balance Calculation
1. Calculate net balance for each person:
net[person] = total_owed_to_them - total_they_owe
2. Separate into two lists:
- Debtors (negative balance): owe money
- Creditors (positive balance): are owed money
3. Match largest debtor with largest creditor:
- Transfer min(|debtor|, creditor) from debtor → creditor
- Update both balances
- Repeat until all balances = 0
Example
Group: Trip to NYC
Expenses: Alice paid $120, Bob paid $60, Carol paid $30
Split equally → each owes $70
Net balances:
Alice: +$50 (paid $120, owes $70) → creditor
Bob: -$10 (paid $60, owes $70) → debtor
Carol: -$40 (paid $30, owes $70) → debtor
Simplification:
Bob → Alice: $10
Carol → Alice: $40
Result: 2 transactions instead of 3 pairwise debts
Java Implementation
public class DebtSimplifier {
public List<Settlement> simplifyDebts(String groupId,
Map<String, Double> netBalances) {
// netBalances: userId → net amount (positive = owed, negative = owes)
PriorityQueue<Map.Entry<String, Double>> debtors = new PriorityQueue<>(
Comparator.comparingDouble(Map.Entry::getValue) // most negative first
);
PriorityQueue<Map.Entry<String, Double>> creditors = new PriorityQueue<>(
(a, b) -> Double.compare(b.getValue(), a.getValue()) // most positive first
);
for (Map.Entry<String, Double> entry : netBalances.entrySet()) {
double balance = Math.round(entry.getValue() * 100.0) / 100.0;
if (balance < -0.01) {
debtors.add(Map.entry(entry.getKey(), balance));
} else if (balance > 0.01) {
creditors.add(Map.entry(entry.getKey(), balance));
}
}
List<Settlement> settlements = new ArrayList<>();
while (!debtors.isEmpty() && !creditors.isEmpty()) {
Map.Entry<String, Double> debtor = debtors.poll();
Map.Entry<String, Double> creditor = creditors.poll();
double debtorAmount = Math.abs(debtor.getValue());
double creditorAmount = creditor.getValue();
double settleAmount = Math.min(debtorAmount, creditorAmount);
settlements.add(new Settlement(
debtor.getKey(),
creditor.getKey(),
Math.round(settleAmount * 100.0) / 100.0
));
double remainingDebtor = debtorAmount - settleAmount;
double remainingCreditor = creditorAmount - settleAmount;
if (remainingDebtor > 0.01) {
debtors.add(Map.entry(debtor.getKey(), -remainingDebtor));
}
if (remainingCreditor > 0.01) {
creditors.add(Map.entry(creditor.getKey(), remainingCreditor));
}
}
return settlements;
}
}
public class BalanceService {
private final Map<String, Map<String, Map<String, Double>>> groupBalances
= new ConcurrentHashMap<>(); // groupId → {userId → {userId → amount}}
public void updateBalance(String groupId, String creditor, String debtor, double amount) {
groupBalances
.computeIfAbsent(groupId, k -> new ConcurrentHashMap<>())
.computeIfAbsent(debtor, k -> new ConcurrentHashMap<>())
.merge(creditor, amount, Double::sum);
}
public Map<String, Double> getNetBalances(String groupId) {
Map<String, Double> netBalances = new HashMap<>();
Map<String, Map<String, Double>> balances = groupBalances.get(groupId);
if (balances == null) return netBalances;
for (Map.Entry<String, Map<String, Double>> debtor : balances.entrySet()) {
for (Map.Entry<String, Double> entry : debtor.getValue().entrySet()) {
String creditor = entry.getKey();
double amount = entry.getValue();
netBalances.merge(debtor.getKey(), -amount, Double::sum);
netBalances.merge(creditor, amount, Double::sum);
}
}
return netBalances;
}
public List<Settlement> getSimplifiedDebts(String groupId) {
Map<String, Double> netBalances = getNetBalances(groupId);
return new DebtSimplifier().simplifyDebts(groupId, netBalances);
}
}
public class SettlementService {
private final BalanceService balanceService;
private final NotificationService notificationService;
public void recordSettlement(Settlement settlement) {
// Mark settlement as completed
settlement.setStatus(SettlementStatus.COMPLETED);
// Update balances
balanceService.updateBalance(
settlement.getGroupId(),
settlement.getToUser(),
settlement.getFromUser(),
-settlement.getAmount() // reduce debt
);
// Notify both parties
notificationService.notifySettlement(settlement.getFromUser(), settlement);
notificationService.notifySettlement(settlement.getToUser(), settlement);
}
}
Follow-ups
Follow-ups
1. Currency Handling
Expense in USD, users pay in EUR, INR, GBP
Approach:
- Store all amounts in base currency (USD)
- Use exchange rate API for conversion
- Show users their local currency equivalent
- Settlements can be in different currencies
2. Multi-Currency Settlement
public class CurrencyService {
private final ExchangeRateService rateService;
public double convert(double amount, String from, String to) {
double rate = rateService.getRate(from, to);
return Math.round(amount * rate * 100.0) / 100.0;
}
}
3. Group Management
Create Group → Add Members → Add Expenses → View Balances → Settle
Composite Pattern for groups:
- Group contains Users and sub-Groups
- Sub-groups can have separate expenses
- Total group balance = sum of all sub-group balances
4. Notification System
| Event | Notification |
|---|---|
| Added to expense | "Alice added 'Dinner' - you owe $15" |
| Settled up | "Bob settled $20 with you" |
| Added to group | "Welcome to 'Trip to NYC'" |
| Expense reminder | "You owe Carol $25 for groceries" |
5. Analytics & Reports
- Monthly summary: who spent the most, per category
- Split fairness: who over/under pays across time
- Export: PDF/CSV for tax purposes
- Trends: spending patterns, common expense types
6. Class Diagram
┌──────────┐ ┌─────────────────┐
│ User │────▶│ Group │
└──────────┘ └─────────────────┘
│ │
│ ▼
│ ┌─────────────────┐
│ │ Expense │
│ └─────────────────┘
│ │
│ ▼
│ ┌─────────────────┐
│ │ SplitStrategy │
│ │ (Strategy) │
│ └─────────────────┘
│ │
│ ┌────────────┼────────────┐
▼ ▼ ▼ ▼
┌────────┐┌───────┐┌──────────┐┌────────┐
│ Equal ││Exact ││Percentage││Shares │
└────────┘└───────┘└──────────┘└────────┘
┌──────────────┐ ┌─────────────────┐
│BalanceService│────▶│DebtSimplifier │
└──────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Settlement │
└─────────────────┘
Practice Problems
Design a scalable Splitwise system. Cover high-level architecture, data model, and API design.
Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliabilityHow would you scale Splitwise to handle 10x the current load? Identify bottlenecks and solutions.
Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decompositionAnalyze potential failure modes for Splitwise and design mitigation strategies.
Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradationQuiz
1. In Splitwise, how do you calculate net balances for a group?
2. Why use debt simplification instead of settling all pairwise debts?
3. Which design pattern is used for different split types (EQUAL, EXACT, PERCENTAGE, SHARES)?
4. In the debt simplification algorithm, how are settlements generated?
5. What happens to balances when an expense is added to a group?
Flashcards
Question
What are the four split types in Splitwise?
Click to reveal answer
Answer
EQUAL (divide equally), EXACT (each pays specific amount), PERCENTAGE (each pays % of total), SHARES (divide by ratio). Each implements SplitStrategy interface.
Question
How does debt simplification work?
Click to reveal answer
Answer
1) Calculate net balance for each person. 2) Separate into debtors (negative) and creditors (positive). 3) Match largest debtor with largest creditor. 4) Settle min(|debtor|, creditor). 5) Repeat until all balances are zero.
Question
What data structure stores who owes whom in a group?
Click to reveal answer
Answer
Map<UserId, Map<UserId, Double>> - a nested map where outer key is debtor, inner key is creditor, value is amount owed. This allows O(1) lookup and updates.
Question
Why is the balance system eventually consistent rather than strongly consistent?
Click to reveal answer
Answer
Balance updates are not time-critical. Users can view approximate balances immediately while background jobs reconcile. Strong consistency would require distributed locks and reduce performance.
Question
What design patterns are used in Splitwise?
Click to reveal answer
Answer
Strategy (split types), Composite (group hierarchy with sub-groups), Observer (balance update notifications), Factory (creating expenses with different split strategies).
Revision Notes
Key Takeaways
- 1.Use Strategy pattern for split types - each split algorithm is independent and interchangeable
- 2.Debt simplification minimizes transactions by matching largest debtor with largest creditor
- 3.Balance tracking uses nested maps: Map<Debtor, Map<Creditor, Amount>>
- 4.Eventual consistency is acceptable for balances - not time-critical like ride-sharing
- 5.Group hierarchy uses Composite pattern - sub-groups inherit expenses from parent
Interview Tips
- •Start with core entities (User, Group, Expense) and their relationships
- •Explain balance calculation with a concrete example: 'Alice paid $120, Bob $60, Carol $30'
- •Walk through debt simplification step-by-step - interviewers want to see the algorithm
- •Discuss split types with examples: EQUAL is simplest, SHARES needs ratio calculation
- •Mention scalability: balances can be computed on-demand or cached, settlements are write-heavy
- •Address edge cases: what if someone leaves a group with outstanding debts? currency conversion?
Cheat Sheet
Splitwise LLD - Cheat Sheet
Core Entities
- User: id, name, balances per group
- Group: id, name, members[], expenses[]
- Expense: amount, paidBy, splitAmong, splitType, splitDetails
- Settlement: fromUser, toUser, amount, status
Balance Tracking
Map<GroupId, Map<Debtor, Map<Creditor, Amount>>>>
When expense added:
- Payer's balance += sum of all shares
- Each debtor's balance -= their share
Debt Simplification Algorithm
- Calculate net balance per person
- Separate into debtors (negative) and creditors (positive)
- Match largest debtor ↔ largest creditor
- Settle min(|debtor|, creditor)
- Repeat until all zero
Split Types
| Type | Input | Formula |
|---|---|---|
| EQUAL | count | amount / count |
| EXACT | amounts[] | directly provided |
| PERCENTAGE | percentages[] | amount × percentage |
| SHARES | shares[] | amount × (share / total_shares) |
Design Patterns
| Pattern | Usage |
|---|---|
| Strategy | Split algorithms (Equal, Exact, Percentage, Shares) |
| Composite | Group hierarchy (groups contain users and sub-groups) |
| Observer | Notify users of balance changes |
| Factory | Create expenses with appropriate split strategy |
Settlement Flow
- Get net balances for group
- Run debt simplification algorithm
- Display simplified debts to user
- User confirms settlement
- Update balances, mark as completed