Compile Ready
All low level design problems
Low Level Design/Intermediate/Core Systems

Design Splitwise

Groups, expenses, and split strategies (equal/exact/percent) with a balance-simplification engine.

Intermediate 55m interview 14m read High frequency Popularity 91
Strategy Observer Factory Method Amazon Uber Flipkart PayPal

Problem Statement

Design the object model for a Splitwise-style expense sharing system. Users can join groups, one member can record an expense paid on behalf of others, and the system maintains net balances so everyone can see who owes whom.

The core design must support equal, exact amount, and percentage splits, update balances whenever an expense is committed, and simplify debts into the smallest practical list of settle-up payments.

Business context

Splitwise is a favorite intermediate LLD problem at product and fintech companies because it looks like simple bookkeeping but quickly exposes design quality. Good candidates separate split calculation from expense orchestration, keep money arithmetic precise, preserve the zero-sum balance invariant, and describe how settlements are derived from balances rather than stored as a second source of truth.

Functional Requirements

  • Create users with stable identity, display name, and email.

  • Create a group and add users as members.

  • Record an expense with payer, amount, and a list of participant splits.

  • Support equal, exact, and percentage split strategies.

  • Validate that all split shares add up exactly to the expense amount.

  • Maintain a net balance sheet for the group after every committed expense.

  • Show a member's current net balance.

  • Generate simplified settle-up payments from current balances.

  • Return immutable snapshots for history, members, and balances so callers cannot corrupt state.

Non-Functional Requirements

Money correctness

Use BigDecimal with two-decimal rounding. Never model balances with floating point numbers.

Consistency

After every balance update, the sum of all net balances must be zero. Any non-zero total is a domain bug.

Extensibility

Adding a new split mode such as shares, weights, or caps should mean adding a Split subclass, not editing expense validation.

Thread safety at aggregate boundaries

Group, ExpenseService, and BalanceSheet synchronize mutations so member checks, expense writes, and balance updates cannot interleave incorrectly.

Defensive reads

Membership, history, and balance snapshots are copied before exposure to keep the in-memory model encapsulated.

Requirement Clarification

QShould users belong to multiple groups?

The core model designs one group at a time. A production service can keep many Group instances and route requests by group id.

QDoes the payer have to be included in the splits?

No. The reference model supports both cases. The payer is credited for the full paid amount, and every listed split user is debited for their share.

QDo we store every pairwise debt edge?

No. Store only net balances per user. Pairwise payments are derived by SettlementService when the user asks to settle up.

QHow exact must percentage splits be?

The base implementation rounds each participant share to two decimals and then validates the sum equals the expense amount. In production, remainder allocation should be explicit.

QIs payment execution in scope?

No. Payment is a settlement instruction. Integrating banks or wallets is a boundary adapter and appears as a follow-up.

UML Class Diagram

Rendering diagram…
The service creates valid expenses, the split hierarchy owns share calculation, the balance sheet owns the zero-sum ledger, and settlement is derived on demand.

Sequence Diagram

Rendering diagram…
Expense creation, balance observation, and debt simplification are separate phases. That separation keeps validation, ledger mutation, and settlement policy independently testable.

Entity Identification

User

Identity value object for a member. Equality and hashing use id, which keeps balance maps stable even when display details change.

iddisplayNameemail

Group

Aggregate for membership and the group's BalanceSheet. It exposes synchronized membership operations and owns the single ledger for the group.

idnamemembersbalanceSheet

Split

Abstract split strategy. It binds a participant user and defines shareOf and validate so each concrete split mode owns its own math.

usershareOf(totalAmount, splitCount)validate(totalAmount)

EqualSplit

Concrete strategy that divides the total amount evenly across the number of split entries, rounding to two decimals.

user

ExactSplit

Concrete strategy that stores a fixed owed amount for the participant and rejects negative or over-large shares.

useramount

PercentSplit

Concrete strategy that stores a percentage and derives the participant amount from the expense total.

userpercent

Expense

Immutable transaction record. It validates positive amount, non-empty splits, each split's rule, and the final sum before the ledger can observe it.

idpayeramountsplitscreatedAt

BalanceSheet

Synchronized net-balance ledger. Applying an expense credits the payer, debits participants, removes zero balances, and enforces total zero.

netBalances

ExpenseService

Application service and factory method boundary. It validates membership, creates Expense ids, applies the expense to the sheet, stores history, and starts settlement.

groupexpenses

SettlementService

Simplification algorithm. It converts positive balances into creditors, negative balances into debtors, and greedily emits payments between the largest remaining sides.

simplify(balances)

Payment

Settle-up instruction from one user to another for a precise amount. It is derived from balances and not stored as primary ledger truth.

fromtoamount

Design Patterns Used

Strategy

Split is the strategy abstraction. EqualSplit, ExactSplit, and PercentSplit vary share calculation and validation while Expense treats them uniformly.

Observer

BalanceSheet is the ledger observer of committed expenses. The reference implementation wires the notification synchronously through ExpenseService.addExpense with apply(expense), keeping balance mutation outside Expense itself.

Factory Method

ExpenseService.addExpense is the creation boundary for valid Expense objects: it checks membership, generates the id, constructs the expense, applies it, and returns the committed aggregate.

Step-by-Step Design

  1. 1Start with stable user identity and group membership

    User equality is based on id, not email or display name. Group keeps members in a set and synchronizes reads and writes so membership checks are consistent during expense creation.

  2. 2Make split modes pluggable strategies

    Put the variable math behind Split.shareOf and Split.validate. Expense can validate any split list without switch statements.

    public abstract class Split {
        private final User user;
    
        public abstract BigDecimal shareOf(BigDecimal totalAmount, int splitCount);
        public abstract void validate(BigDecimal totalAmount);
    }
  3. 3Validate each expense before touching balances

    Expense is immutable and validates that the amount is positive, the split list is non-empty, and all calculated shares sum to the paid amount.

    BigDecimal total = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
    for (Split split : splits) {
        split.validate(amount);
        total = total.add(split.shareOf(amount, splits.size()));
    }
    if (total.compareTo(amount) != 0) {
        throw new IllegalArgumentException();
    }
  4. 4Use the service as the factory and transaction boundary

    ExpenseService.addExpense checks that payer and participants are group members, creates a UUID-backed Expense, applies it to the ledger, appends history, and returns the committed object.

    public synchronized Expense addExpense(User payer, BigDecimal amount, List<Split> splits) {
        ensureMember(payer);
        for (Split split : splits) ensureMember(split.getUser());
        Expense expense = new Expense(UUID.randomUUID().toString(), payer, amount, splits);
        group.getBalanceSheet().apply(expense);
        expenses.add(expense);
        return expense;
    }
  5. 5Update net balances as an observed side effect

    BalanceSheet.apply credits the payer for the full amount and debits every split user for their share. Removing zero entries keeps snapshots compact, and the total-zero assertion catches drift.

  6. 6Derive settle-up payments from balances

    SettlementService does not mutate the ledger. It separates creditors and debtors into priority queues, matches the largest credit with the largest debt, emits a Payment, and repeats until one side is empty.

  7. 7Keep read APIs defensive

    getMembers, history, and snapshot return unmodifiable copies. This is important in LLD interviews because it proves callers cannot bypass domain methods and edit internal state.

Complete Java Implementation

Loading…

Explanation of Every Class

User

Final identity class with id, displayName, and email. Equality and hash code depend only on id, which is exactly what a ledger map needs.

Group

Owns a synchronized LinkedHashSet of members and a single BalanceSheet. It gives the service a membership guard without exposing mutable internal state.

Split

Abstract base for split strategies. It stores the participant User, declares shareOf and validate, and provides money for two-decimal rounding.

EqualSplit

Concrete split strategy that divides the total amount by split count using two-decimal HALF_UP rounding. It only needs to ensure the expense amount is positive.

ExactSplit

Concrete split strategy with a fixed amount. It rejects negative shares and shares larger than the total expense.

PercentSplit

Concrete split strategy with a percent field. It validates the percentage range and converts it to money by multiplying the expense amount.

Expense

Immutable expense record with generated id, payer, rounded amount, copied splits, and timestamp. Construction fails unless split shares sum exactly to the amount.

BalanceSheet

Synchronized ledger of User to net BigDecimal. apply credits the payer, debits participants, removes zero balances, and asserts that the ledger remains zero-sum.

ExpenseService

Application service for a single group. It validates membership, factory-creates expenses with UUIDs, updates the balance sheet, stores history, answers balances, and delegates settlement.

SettlementService

Stateless debt simplifier. It builds creditor and debtor priority queues from a snapshot and repeatedly emits the minimum payment between the largest remaining amounts.

BalanceNode

Private helper inside SettlementService that pairs a User with an absolute amount for priority queue ordering.

Payment

Package-private settlement instruction with from, to, and amount. describe renders a human-readable instruction without exposing ledger mutation.

Dry Run

Sample input

Group members: Asha, Ben, Chen. Expense 1: Asha pays 300.00 split equally across all three. Expense 2: Ben pays 120.00 split exactly as Asha 50.00 and Chen 70.00.

StepActionAshaBenChenOutput
1Add members0.000.000.00All balances start at zero
2Asha pays 300.00 equally+200.00-100.00-100.00Asha credited 300.00 and debited 100.00
3Ben pays 120.00 exact+150.00+20.00-170.00Asha debited 50.00, Chen debited 70.00
4settleUp()+150.00+20.00-170.00Chen -> Asha 150.00; Chen -> Ben 20.00

The balance sheet stores net positions, not pairwise edges. Asha and Ben are creditors after both expenses; Chen is the only debtor, so settlement emits two payments.

Complexity Analysis

OperationTimeSpaceNote
addExpenseO(S)O(S)S split entries. Membership checks are set lookups, validation iterates splits, and the expense stores a copied split list.
balanceOfO(1)O(1)Hash map lookup in the balance sheet.
historyO(E)O(E)Returns a defensive copy of E expenses.
snapshotO(U)O(U)Copies balances for U users with non-zero positions.
settleUpO(U log U)O(U)Builds debtor and creditor priority queues and emits at most O(U) payments.

For interview scale, the hot path is addExpense, linear in participants on that expense. settleUp is usually less frequent and depends on users with non-zero balances, not total expenses recorded.

Extensibility

New split strategy

Add a new Split subclass such as ShareSplit, WeightSplit, or CappedSplit. Expense and BalanceSheet keep calling shareOf and validate.

Multi-group application

Put Group instances behind a repository keyed by group id. ExpenseService can become request-scoped or receive the group for each command.

Real payment execution

Keep Payment as a domain instruction and add a PaymentProcessor adapter for wallets, UPI, cards, or bank transfers.

Expense categories and attachments

Add optional fields to Expense or a side object for category, notes, and receipt URLs without changing split math or settlement.

Notifications

Generalize the synchronous balance update into an observer list so expense-created events can update balances, send push notifications, and write audit logs.

Alternative Designs

Pairwise debt matrix

Store what each user owes every other user directly, updating pairwise edges on every expense.

Tradeoffs

Makes some user-to-user views fast, but creates O(U squared) state and makes simplification more complex. Net balances are a cleaner source of truth.

Event-sourced ledger

Append immutable expense events and recompute balances from the event log or maintain projections.

Tradeoffs

Excellent auditability and replay, but more infrastructure and eventual consistency. For an LLD interview, an in-memory balance sheet is clearer.

Explicit observer interface

Define an ExpenseObserver interface and let BalanceSheet, notification senders, and audit writers subscribe to committed expenses.

Tradeoffs

More extensible than a direct apply call, but adds ceremony. The reference keeps the observer relationship synchronous and simple.

Stored settlements

Persist generated Payment instructions as settlement sessions.

Tradeoffs

Useful for payment workflows, but dangerous if treated as truth. Balances must remain the canonical ledger until payments are actually recorded.

Common Mistakes

  • ×

    Using double or float for money and then fighting rounding bugs.

  • ×

    Keeping pairwise debts as the primary source of truth when net balances are enough for settlement.

  • ×

    Putting equal, exact, and percentage logic in ExpenseService with conditionals instead of a Split strategy hierarchy.

  • ×

    Forgetting to validate that split shares sum to the expense amount.

  • ×

    Allowing non-members to appear in an expense because only the payer was checked.

  • ×

    Updating history before balance application succeeds, leaving a committed expense without ledger changes.

  • ×

    Returning mutable lists or maps from Group, ExpenseService, or BalanceSheet.

  • ×

    Treating simplified Payment instructions as already-settled money movement.

Follow-up Interview Questions

QHow would you record that Chen actually paid Asha 150.00?

Model settlement as another ledger event: payer Chen, receiver Asha, amount 150.00. Apply it by debiting Asha's credit and reducing Chen's debt, then keep an audit record.

QHow do you support recurring expenses like monthly rent?

Add a scheduler that creates normal Expense objects from a recurrence rule. The ledger should not care whether an expense was manual or scheduled.

QHow would you make percentage splits robust when rounding leaves one cent?

Choose a deterministic remainder policy, such as assigning leftover cents to the payer or to participants sorted by id, and test it explicitly.

QCan this design support individual friendships outside groups?

Yes. Represent a one-to-one group or introduce a Ledger abstraction shared by groups and friendships. The split, expense, and settlement logic remains reusable.

QWhat changes for millions of users?

Persist users, expenses, and balances; shard by group id; make addExpense transactional; publish expense-created events for async projections and notifications.

Production Considerations

Transactional persistence

Store expense history and balance deltas in one database transaction. A committed expense without its balance update is an accounting incident.

Audit trail

Keep immutable expense and settlement events, including who created them and when. Financial products need explainability and rollback paths.

Idempotency

Accept a client request id for addExpense so retries after network failures do not create duplicate expenses.

Currency and locale

Add a currency field and use currency-specific scales. Do not mix balances across currencies without an explicit conversion event.

Notifications and projections

Publish an expense-created event after commit. Balance projections, push notifications, email, and analytics can consume it independently.

Privacy and access control

Verify group membership for every read and write. Expense history and balances are sensitive financial data.

What Interviewers Look For

  • Did the candidate identify Split as the Strategy seam and avoid switch-heavy split calculation?

  • Did they protect the zero-sum balance invariant after every expense?

  • Did they separate net balances from derived settle-up payments?

  • Did they use precise money types and discuss rounding edge cases?

  • Did they keep ExpenseService as the consistency boundary for membership, creation, ledger update, and history?

  • Can they explain the greedy settlement algorithm and its complexity?

Quiz

0/5 answered

  1. 1.Why is **Split** an abstract class with **shareOf** and **validate**?

  2. 2.What invariant does **BalanceSheet.apply** verify after updating balances?

  3. 3.Why does **settleUp** use priority queues of creditors and debtors?

  4. 4.Which class acts as the factory method boundary for creating committed expenses?

  5. 5.Why are **history** and **snapshot** returned as defensive copies?

Practice Variants

Add share-based splits

Beginner

Implement ShareSplit where each participant has N shares and pays amount times their share divided by total shares. Keep Expense unchanged.

Record actual settlements

Intermediate

Add a command that records a real payment between two users and applies it to the balance sheet as a ledger event.

Multi-currency groups

Advanced

Allow a group to track balances per currency and reject settlement across currencies unless an explicit conversion event exists.

Expense observers

Intermediate

Introduce an ExpenseObserver interface so balance updates, notifications, analytics, and audit logging subscribe to the same committed expense event.

Flashcards

Cheat Sheet

Entities: User, Group, Split hierarchy, Expense, BalanceSheet, ExpenseService, SettlementService, Payment.

Patterns: Strategy for split calculation; Observer-style balance update on committed expense; Factory Method through ExpenseService.addExpense.

Core flow: addExpense = validate members -> create Expense -> validate split totals -> apply BalanceSheet -> append history.

Balance rule: payer gets credited by the paid amount; every split user is debited by their computed share; all balances must sum to zero.

Settlement: derive payments from net balances using creditor and debtor priority queues. Do not store pairwise debts as primary truth.

Complexity: addExpense O(S), balanceOf O(1), snapshot O(U), settleUp O(U log U).

Interview hooks: money precision, rounding remainders, idempotency, settlement recording, multi-currency, and observer-based notifications.

References