Senior Design Review

Splitwise LLD

A careful walkthrough of the expense-splitting design: users join a group, expenses are split by strategy, the group ledger stores net balances, and settlement moves users back toward zero.

Strategy Split calculation
Singleton Application service
Ledger Group-wise balance

What The Code Does

The design is centered around a group ledger

01

User and Group membership

`User` represents identity. `GroupManager` owns the users inside one group and initializes each user's group balance at zero. Overriding `equals()` and `hashCode()` is important because users are map keys.

02

Expense creation

When an expense is added, the service asks the selected split strategy to produce a user-to-share map. The expense is then stored with paid-by, amount, and split details.

03

Balance update

The payer receives credit for the full amount. Every participant is debited by their share. The final number is net balance: positive means receive, negative means pay.

Pattern Analysis

Why these design patterns fit the Splitwise problem

Strategy Pattern

SplitStrategy, EqualSplit, PercentageSplit

The algorithm for calculating shares changes depending on the expense type. Equal and percentage split should not be hard-coded inside `GroupManager`; that would make the group class grow every time a new split type is introduced. Strategy keeps the calculation replaceable.

interface SplitStrategy {
    Map<User, Double> splitAmount(double amount, List<User> users);
}

class EqualSplit implements SplitStrategy {
    public Map<User, Double> splitAmount(double amount, List<User> users) {
        double perUserAmount = amount / users.size();
        Map<User, Double> result = new HashMap<>();
        for (User user : users) {
            result.put(user, perUserAmount);
        }
        return result;
    }
}

Singleton Pattern

SplitwiseService

`SplitwiseService` is the application entry point for adding expenses and settling up. In this simplified in-memory version, a Singleton gives one shared coordinator. In production, this can become a dependency-injected service instead of a static Singleton.

class SplitwiseService {
    private static SplitwiseService instance;

    private SplitwiseService() {}

    public static synchronized SplitwiseService getInstance() {
        if (instance == null) {
            instance = new SplitwiseService();
        }
        return instance;
    }
}

Ledger Model

Group-wise balance tracking

The balance sheet is the core Splitwise logic. The balance sheet is not storing each transaction as debt edges. It stores net group balance per user, which makes profile and group summaries fast to display.

private void updateBalanceSheet(User paidBy, double amount, Map<User, Double> splitMap) {
    balanceSheet.put(paidBy, balanceSheet.getOrDefault(paidBy, 0.0) + amount);

    for (Map.Entry<User, Double> entry : splitMap.entrySet()) {
        User user = entry.getKey();
        double share = entry.getValue();
        balanceSheet.put(user, balanceSheet.getOrDefault(user, 0.0) - share);
    }
}

Thread Safety Boundary

synchronized group mutations

Expense addition, user removal, and settlement mutate shared group state. Marking these methods `synchronized` ensures a single group ledger update completes before another thread changes the same ledger. This protects the balance sheet from race conditions in this in-memory design.

public synchronized void addExpense(...) {
    Map<User, Double> splitMap = splitStrategy.splitAmount(amount, users);
    expenses.add(new Expense(expenseId, paidBy, amount, splitMap));
    updateBalanceSheet(paidBy, amount, splitMap);
}

Execution Trace

How the balance changes in the sample

EXP01

SAM pays 1000 equally

SAM gets credited 1000, then pays his own share 333.33. Net SAM: +666.67. PAM and Kris: -333.33 each.

EXP02

Kris pays 4000 equally

Kris gets credited 4000 and owes 1333.33 share. His net gain is +2666.67, then it merges with old balance.

EXP03

PAM pays 2000 by percentage

The strategy maps SAM to 10%, PAM to 40%, and Kris to 50%. This keeps custom split logic outside the group ledger.

Settle Up

PAM pays Kris 1000

PAM's balance moves up by 1000 because she paid. Kris's balance moves down by 1000 because he received.

Runnable Java

Full code from the walkthrough

Main.java

The compiler-ready code demonstrates equal split, percentage split, group-wise ledger, and settlement.

Embedded Compiler

Paste the Java source into the compiler and run the sample flow.

Design Review Notes

Good: Split calculation is separated from balance tracking.

Good: `User` is safe as a map key because identity methods are implemented.

Good: Group mutations are synchronized for this in-memory implementation.

Next: Add an Observer-based notification system when expenses and settlements occur.

Next: Add an `ExactSplit` strategy and a settlement optimizer for minimum transactions.

Interview Ready

Notes For Interview

How to explain the design

This Splitwise LLD is centered around a group ledger. Users join a group, expenses are added by one payer, and the selected split strategy calculates how much each participant owes. The group maintains a net balance sheet where positive means the user should receive, negative means the user should pay, and zero means the user is settled.

The key interview point is that split logic is not hard-coded inside the group. It is delegated to `SplitStrategy`, so Equal, Percentage, Exact, or Share split can be added without changing the core ledger update logic.

Flow Diagram

User adds expense
Validate payer and group members
Apply SplitStrategy
Create Expense record
Credit payer full amount
Debit every participant share
Update group balance sheet

UML Diagram: IS-A and HAS-A

Group
GroupManager
List<User>
List<Expense>
Map<User, Double>
SplitStrategy
EqualSplit
PercentageSplit
Expense
paidBy: User
splitMap: Map<User, Double>
SplitwiseService
GroupManager

Core classes

  • User: represents identity using `id` and `name`; equality is based on `id` because users are map keys.
  • Expense: stores expense id, payer, amount, and calculated user-to-share map.
  • GroupManager: owns users, expenses, balance sheet, expense updates, and settlement.
  • SplitStrategy: abstracts split calculation through `EqualSplit` and `PercentageSplit`.
  • SplitwiseService: acts as the application coordinator in this in-memory version.

Balance logic

When SAM pays `1000` for three people, each share is `333.33`. SAM gets credited with `1000`, then SAM, PAM, and Kris are each debited by `333.33`. Final balance: SAM receives `666.67`, PAM pays `333.33`, and Kris pays `333.33`.

During settlement, if PAM pays Kris `1000`, PAM's balance increases because her debt reduces, and Kris's balance decreases because his receivable reduces.

Design patterns

  • Strategy Pattern: keeps split calculation replaceable and open for extension.
  • Singleton Pattern: gives one shared `SplitwiseService` coordinator for the demo.
  • Ledger Model: stores net balances instead of pairwise debt edges.
  • Synchronized methods: protect in-memory group mutations from race conditions.

Why these patterns are used

The main changing behavior in Splitwise is how an expense is divided. Equal split, percentage split, exact split, and share split all produce the same output type: a map of user to amount owed. Strategy Pattern is used because the group should not contain `if-else` blocks for every split type. If a new split type is introduced, we add a new strategy class and the balance update logic remains untouched.

The ledger model is used because the UI usually needs to answer "how much does this user owe or receive?" quickly. Instead of storing every pairwise debt edge, we maintain one net value per user inside the group. This makes group summaries simple. Pairwise settlement can be derived later using a separate settlement optimization algorithm.

Algorithm explanation

The expense update algorithm has two phases. First, the split strategy calculates each participant's share. Second, the ledger is updated by crediting the payer with the full amount and debiting every user by their share. This works because the payer also participates in the split, so their final net balance becomes `amount paid - own share`.

Settlement uses a reverse adjustment. If a debtor pays a receiver, the payer's balance increases toward zero and the receiver's balance decreases toward zero. For minimum transactions, a production system can separate positive balances and negative balances, then greedily match the largest payer with the largest receiver until all balances become zero.

Strong interview answer

"I would model Splitwise at the group level. Each group has users, expenses, and a balance sheet. When an expense is added, the payer gets credited with the full amount and every participant gets debited by their share. Split calculation is delegated to a strategy, so new split types can be added without modifying the group ledger logic."

Production improvements

  • Use `BigDecimal` or smallest currency unit instead of `double` for money.
  • Add `ExactSplit`, `ShareSplit`, and a settlement optimizer for minimum transactions.
  • Persist users, groups, expenses, and settlements in a database.
  • Add access control so only group members can add expenses.
  • Add Observer-based notifications when expenses or settlements occur.
  • Replace Singleton with dependency injection in a production service.

Weakness to mention

The current implementation uses `double`, which is risky for money because of precision issues. Also, `PercentageSplit` checks that percentages sum to `100`, but it should more strictly validate that the percentage map matches the group members.