Skip to content
intermediatePhase 50 · LLD Practice

Splitwise

Design an expense splitting system with debts and settlements.

1h 30m
0 problems
Topic Progress0%

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

0/3solved
Design Splitwise System

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 & reliability
Splitwise Scaling

How 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 decomposition
Splitwise Failure Modes

Analyze 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 degradation

Quiz

1. In Splitwise, how do you calculate net balances for a group?

Question 1 options

2. Why use debt simplification instead of settling all pairwise debts?

Question 2 options

3. Which design pattern is used for different split types (EQUAL, EXACT, PERCENTAGE, SHARES)?

Question 3 options

4. In the debt simplification algorithm, how are settlements generated?

Question 4 options

5. What happens to balances when an expense is added to a group?

Question 5 options

Flashcards

Question

What are the four split types in Splitwise?

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?

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?

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?

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?

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

  1. Calculate net balance per person
  2. Separate into debtors (negative) and creditors (positive)
  3. Match largest debtor ↔ largest creditor
  4. Settle min(|debtor|, creditor)
  5. 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

  1. Get net balances for group
  2. Run debt simplification algorithm
  3. Display simplified debts to user
  4. User confirms settlement
  5. Update balances, mark as completed