LLD Hub
lldoopgraph-algorithmstrategy

Splitwise Low Level Design — Expense Splitter LLD Interview Guide

Design a Splitwise-like app with equal, exact, and percentage splits, balance tracking, and debt simplification algorithm. Asked at fintech SDE interviews.

14 April 2025·8 min read

Practice this problem

Expense Splitter (Splitwise-like) — get AI-scored feedback on your solution

Solve it →

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 writeif (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).

Ready to practice?

Submit your solution and get AI-scored feedback on OOP, SOLID principles, design patterns, and code quality.

Solve Expense Splitter (Splitwise-like)