Design Splitwise
Expense sharing with equal/exact/percent splits and balance sheet — popular at startups, fintech, and Amazon/Flipkart “design an app” LLD prompts.
ExpenseService records who paid and how cost is split via SplitStrategy. Maintain net balance per user in a group; optional settle-up minimizes cash flow (mention as extension).
The interview question
“Design an expense sharing app like Splitwise.” Users in a group add expenses; system tracks who owes whom.
Core: add expense, split logic, view balances — not payment gateway integration in v1.
Clarifying questions (ask before coding)
Group with member list; balances scoped per group.Requirements & assumptions (state aloud)
- addExpense(paidBy, amount, participants, splitType) updates balances.
- getBalances(groupId) → who owes whom (net per user).
- Equal split: amount / n; handle rounding (pennies to payer).
- v1: in-memory, single process.
High-level design
On expense: payer credit += amount; each participant debit += share. Net map user → balance (positive = others owe them).
SplitStrategy.split(total, users) returns Map<user, share>.
User,Group,Expense,ExpenseSplitSplitStrategy+EqualSplitStrategyExpenseService,BalanceSheet
UML class diagram (optional)
Emphasize Strategy for split algorithms and facade ExpenseService.
Design patterns used (and why)
| Pattern | Where | Why (interview soundbite) |
|---|---|---|
| 🎯Strategy | SplitStrategy | Equal / exact / percent without changing ExpenseService. |
| 🪞Facade | ExpenseService | Single API for add expense + get balances. |
Full solution — domain & split strategy
public class User {
private final String userId;
private final String name;
// Person who pays or owes in a group
public User(String userId, String name) {
this.userId = userId;
this.name = name;
}
// Stable id used as key in balance and split maps
public String getUserId() { return userId; }
}
public class Group {
private final String groupId;
private final List<User> members = new ArrayList<>();
// Expense sharing circle (trip, apartment, etc.)
public Group(String groupId) { this.groupId = groupId; }
// Adds a member to the group
public void addMember(User u) { members.add(u); }
// Read-only member list for split calculations
public List<User> getMembers() { return Collections.unmodifiableList(members); }
// Id scopes balance sheet per trip/household
public String getGroupId() { return groupId; }
}
public interface SplitStrategy {
// Divides total amount into per-user shares (sum should equal total)
Map<String, BigDecimal> split(BigDecimal total, List<User> participants);
}
public class EqualSplitStrategy implements SplitStrategy {
// Each participant owes total / n
@Override
public Map<String, BigDecimal> split(BigDecimal total, List<User> participants) {
if (participants.isEmpty()) return Map.of();
int n = participants.size();
BigDecimal share = total.divide(BigDecimal.valueOf(n), 2, RoundingMode.HALF_UP);
Map<String, BigDecimal> out = new HashMap<>();
for (User u : participants) out.put(u.getUserId(), share);
return out;
}
}Full solution — expense service & balances
public class Expense {
private final String expenseId = UUID.randomUUID().toString();
private final String groupId;
private final String paidByUserId;
private final BigDecimal amount;
private final Map<String, BigDecimal> splits;
// Immutable record of one spend and how it was split
public Expense(String groupId, String paidByUserId, BigDecimal amount, Map<String, BigDecimal> splits) {
this.groupId = groupId;
this.paidByUserId = paidByUserId;
this.amount = amount;
this.splits = Map.copyOf(splits);
}
}
public class BalanceSheet {
private final Map<String, BigDecimal> net = new HashMap<>();
// Credits payer by sum of splits; debits each participant their share
public void applyExpense(String paidBy, Map<String, BigDecimal> splits) {
net.merge(paidBy, sumValues(splits), BigDecimal::add);
for (var e : splits.entrySet())
net.merge(e.getKey(), e.getValue().negate(), BigDecimal::add);
}
// Sums all split amounts (should equal expense total)
private static BigDecimal sumValues(Map<String, BigDecimal> m) {
return m.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
}
// Returns net balance per userId (+ means others owe them)
public Map<String, BigDecimal> getBalances() {
return Collections.unmodifiableMap(net);
}
}public class ExpenseService {
private final Map<String, BalanceSheet> groupBalances = new HashMap<>();
private final SplitStrategy defaultSplit = new EqualSplitStrategy();
// Splits expense equally and updates group balance sheet
public void addExpense(Group group, User paidBy, BigDecimal amount, List<User> participants) {
Map<String, BigDecimal> splits = defaultSplit.split(amount, participants);
BalanceSheet sheet = groupBalances.computeIfAbsent(group.getGroupId(), k -> new BalanceSheet());
sheet.applyExpense(paidBy.getUserId(), splits);
}
// Returns who owes whom for a group (net balances)
public Map<String, BigDecimal> getBalances(String groupId) {
return groupBalances.getOrDefault(groupId, new BalanceSheet()).getBalances();
}
}
public class SplitwiseDemo {
// Alice pays 100 split with Bob — print resulting balances
public static void main(String[] args) {
User alice = new User("u1", "Alice");
User bob = new User("u2", "Bob");
Group trip = new Group("g1");
trip.addMember(alice);
trip.addMember(bob);
ExpenseService svc = new ExpenseService();
svc.addExpense(trip, alice, new BigDecimal("100"), List.of(alice, bob));
System.out.println(svc.getBalances("g1")); // bob owes alice
}
}While you code — questions interviewers ask
Extensions & follow-ups
- Minimize transactions (graph / greedy settle).
- ExactSplitStrategy and PercentSplitStrategy.
- Activity feed (Observer) on new expense.
Common mistakes (fail signals)
- Storing pairwise owed matrix without net simplification (OK v1 but explain scale).
- No validation that splits sum to total.
- Hard-coded equal split only with no Strategy interface.