Back to LLD explorer
Object-Oriented Design• Patterns: Strategy, Factory, Observer

Splitwise (Expense Sharing App)

Hard

Problem Summary

Design an Expense Sharing system (like Splitwise) supporting diverse split methods (Equal, Exact, Percentage), group expense management, balance sheets, and debt simplification.

Functional Scope

  • Flexible split calculations: EQUAL, EXACT, PERCENTAGE.
  • Track pairwise balances across users and inside groups.
  • Input validation: percentage splits must sum to exactly 100%, exact splits to total cost.
  • Graph-based debt simplification algorithm minimizing total transaction count.
  • Direct settlement transactions between members.

Entity-Relationship (ER) Schema

ExpenseManager [1] --> [*] User
ExpenseManager [1] --> [*] Group
Expense [1] --> [*] Split
Expense [1] --> [1] SplitStrategy
SplitStrategy <|.. EqualSplitStrategy

Design Approach

Apply the Strategy pattern for equal, exact, and percentage expense distribution math. Use a min-cash-flow graph algorithm to simplify cyclic debts across group members.

Core Classes & Models

ExpenseManager (Main orchestrator maintaining balances and groups)User (UserId, name, email)Expense (Amount, paidBy, list of splits, strategy)SplitStrategy (EqualSplitStrategy, ExactSplitStrategy, PercentageSplitStrategy)DebtSimplifier (Graph simplification algorithm for minimal cash flow)
Code Blueprint
100 lines
/**
 * Low-Level Design: Splitwise Expense Sharing App
 * Design Patterns Applied:
 *  - Strategy Pattern: Pluggable split calculation algorithms (Equal, Exact, Percentage)
 *  - Factory Pattern: Instantiating Expense and Split instances
 *  - Observer Pattern: Real-time debt updates and settle-up notifications
 */

import java.util.*;

enum SplitType { EQUAL, EXACT, PERCENTAGE }

class User {
    private final String id, name;
    public User(String id, String name) { this.id = id; this.name = name; }
    public String getId() { return id; }
    public String getName() { return name; }
}

class Split {
    private final User user;
    private double amount;
    public Split(User user) { this.user = user; }
    public User getUser() { return user; }
    public double getAmount() { return amount; }
    public void setAmount(double amount) { this.amount = amount; }
}

// --- [DESIGN PATTERN: Strategy] Expense splitting strategy ---
interface SplitStrategy {
    void calculateSplits(double totalAmount, List<Split> splits, List<Double> values);
}

class EqualSplitStrategy implements SplitStrategy {
    @Override
    public void calculateSplits(double totalAmount, List<Split> splits, List<Double> values) {
        double share = totalAmount / splits.size();
        for (Split s : splits) s.setAmount(share);
    }
}

class ExactSplitStrategy implements SplitStrategy {
    @Override
    public void calculateSplits(double totalAmount, List<Split> splits, List<Double> values) {
        for (int i = 0; i < splits.size(); i++) {
            splits.get(i).setAmount(values.get(i));
        }
    }
}

// --- [DESIGN PATTERN: Observer] Balance ledger listener ---
interface BalanceObserver {
    void onBalanceUpdated(String debtor, String creditor, double amount);
}

class LedgerNotifier implements BalanceObserver {
    @Override
    public void onBalanceUpdated(String debtor, String creditor, double amount) {
        System.out.printf("📊 [Ledger] %s owes %s $%.2f%n", debtor, creditor, amount);
    }
}

class SplitwiseService {
    private final Map<String, Map<String, Double>> balanceSheet = new HashMap<>();
    private final List<BalanceObserver> observers = new ArrayList<>();

    public void addObserver(BalanceObserver o) { observers.add(o); }

    public void addExpense(User paidBy, double total, List<Split> splits, SplitStrategy strategy) {
        strategy.calculateSplits(total, splits, Collections.emptyList());

        for (Split split : splits) {
            User owesUser = split.getUser();
            if (!owesUser.getId().equals(paidBy.getId())) {
                updateBalance(owesUser.getName(), paidBy.getName(), split.getAmount());
            }
        }
    }

    private void updateBalance(String debtor, String creditor, double amount) {
        balanceSheet.computeIfAbsent(debtor, k -> new HashMap<>())
                    .merge(creditor, amount, Double::sum);
        for (BalanceObserver o : observers) o.onBalanceUpdated(debtor, creditor, amount);
    }
}

public class Main {
    public static void main(String[] args) {
        SplitwiseService splitwise = new SplitwiseService();
        splitwise.addObserver(new LedgerNotifier());

        User alice = new User("U1", "Alice");
        User bob = new User("U2", "Bob");
        User charlie = new User("U3", "Charlie");

        List<Split> splits = List.of(new Split(alice), new Split(bob), new Split(charlie));
        System.out.println("--- Splitting $90 Dinner Equally ---");
        splitwise.addExpense(alice, 90.0, splits, new EqualSplitStrategy());
    }
}