Sweaty Imposter

Expense Sharing App

Design the core of a Splitwise-style app: users form groups, add expenses paid by one person and split equally, by exact amounts, or by percentage, and see who owes whom. Support settling up and simplifying debts within a group.

Clarifying Questions

  • Can an expense have several payers, or exactly one?
  • Which split types are needed: equal, exact amounts, percentages, or shares?
  • Is everything in one currency, or do groups mix currencies?
  • How do we handle rounding when 100.00 splits three ways?
  • Should simplification minimize the number of transfers, or keep pairwise balances as recorded?

Core Requirements

  • Create groups and add members.
  • Record an expense with a payer, a total, and a split across participants.
  • Support equal, exact, and percentage splits, validated to sum to the total.
  • Show each member's net balance and pairwise debts within a group.
  • Record settlements that reduce balances.
  • Simplify group debts into a small set of suggested transfers.

Canonical class breakdown

User: Identity and display name; holds no balance state of its own.
Group: Owns members and the ordered list of Expenses and Settlements; the boundary for balance computation.
Expense: Immutable record of payer, total in minor units, description, and the computed Splits.
Split: One participant's share of an Expense as an exact amount in minor units.
SplitStrategy (interface) / EqualSplit, ExactSplit, PercentSplit: Turns a total and participant inputs into Splits, validates the inputs, and assigns rounding remainders deterministically.
Settlement: A payment from one member to another that offsets balances; recorded as its own entry.
BalanceLedger: Maintains each member's net balance and pairwise balances, updated incrementally as expenses and settlements post.
DebtSimplifier: Reduces net balances to suggested transfers by repeatedly matching the largest creditor with the largest debtor.
ExpenseService: Use-case layer for add, edit, delete, and settle; the only writer to the ledger.

Java sketchjava

// Java skeleton

enum SplitType { EQUAL, EXACT, PERCENT }

record Money(long minorUnits, String currency) {
    public Money plus(Money o) { ... }
    public Money minus(Money o) { ... }
}

class User {
    private final String userId;
    private final String displayName;
}

class Split {
    private final User participant;
    private final Money share;
}

class Expense {
    private final String expenseId;
    private final User paidBy;
    private final Money total;
    private final String description;
    private final List<Split> splits; // sums exactly to total
    private final String reversesExpenseId; // set on edit or delete
}

class Settlement {
    private final String settlementId;
    private final User from;
    private final User to;
    private final Money amount;
}

interface SplitStrategy {
    // validates inputs, assigns the rounding remainder deterministically
    List<Split> split(Money total, User payer, Map<User, BigDecimal> inputs);
}

class EqualSplit implements SplitStrategy { ... }
class ExactSplit implements SplitStrategy { ... }
class PercentSplit implements SplitStrategy { ... }

class BalanceLedger {
    private final Map<User, Long> netBalance;              // positive = owed money
    private final Map<User, Map<User, Long>> pairwise;     // debtor -> creditor -> amount
    public void post(Expense e) { ... }
    public void post(Settlement s) { ... }
    public long netOf(User u) { ... }
}

record Transfer(User from, User to, Money amount) {}

class DebtSimplifier {
    // greedy: match largest creditor with largest debtor until all are zero
    public List<Transfer> simplify(Map<User, Long> netBalances) { ... }
}

class Group {
    private final String groupId;
    private final Set<User> members;
    private final List<Expense> expenses;    // append-only
    private final List<Settlement> settlements;
    private final BalanceLedger ledger;
}

class ExpenseService {
    private final Map<SplitType, SplitStrategy> strategies;
    private final DebtSimplifier simplifier;
    public Expense addExpense(Group g, User payer, Money total, SplitType type, Map<User, BigDecimal> inputs) { ... }
    public Expense editExpense(Group g, Expense old, Money total, SplitType type, Map<User, BigDecimal> inputs) { /* reversal + new entry */ }
    public Settlement settle(Group g, User from, User to, Money amount) { ... }
    public List<Transfer> suggestTransfers(Group g) { ... }
}

Key Decisions to Defend

  • Money is a long in minor units plus a currency code; floating point drifts the moment 10.00 splits three ways.
  • SplitStrategy is a Strategy, so a new split type (shares, adjustments) adds a class and leaves Expense untouched.
  • Rounding remainders go to a deterministic participant, such as the payer, so every client computes identical balances.
  • Editing an expense posts a reversal and a new entry; the ledger stays append-only and the history stays auditable.
  • Simplification is a view over net balances; it suggests transfers and leaves the recorded expenses intact.

Likely follow-ups

  • · Add multi-currency groups, when do you convert and at what rate?
  • · Two members edit the same expense offline, how do you merge?
  • · Is the greedy simplifier minimal? Show a case where it is not.
  • · Support recurring expenses like rent without duplicating logic.

Dive Deeper

Expense sharing tests money handling and the separation of facts from derived views. Expenses and settlements are facts; balances and simplified transfers are derived from them, and strong candidates keep that line sharp so edits and deletes stay correct. Rounding is where many designs fail: a three-way split of 100.00 must assign the extra cent somewhere, deterministically. Interviewers also probe whether you know that finding the true minimum number of transfers is NP-hard, which is why the greedy match is the practical choice.

All OOD Prompts