Design Splitwise: Machine Coding Round Walkthrough
A complete walkthrough of the Splitwise / expense-sharing machine coding problem: requirements, entities, a Strategy-based split design, balance tracking, debt simplification, runnable Java code, and the follow-up questions interviewers ask.
The expense-sharing app is a classic machine coding problem: small enough to build in 90 minutes, rich enough to show whether you can design extensible classes. This walkthrough follows the time-boxed workflow from our LLD and machine coding guide, with runnable Java at the end.
Step 1: Clarify requirements (10 minutes)
Ask, then write down what's in scope:
- Users can add an expense paid by one user and shared among several.
- Split types: equal, exact amounts, percentages.
- Show balances: for one user, and for everyone.
- Out of scope unless asked: groups, multiple currencies, persistence, settling up via payments.
Typical input in the problem statement looks like:
EXPENSE u1 1000 4 u1 u2 u3 u4 EQUAL
EXPENSE u1 1250 2 u2 u3 EXACT 370 880
EXPENSE u4 1200 4 u1 u2 u3 u4 PERCENT 40 20 20 20
SHOW
SHOW u1
Confirm the output format too, for example "u2 owes u1: 250".
Step 2: Entities and extension points (10 minutes)
| Entity | Responsibility |
|---|---|
User | id, name |
Split | a participant and their share |
SplitStrategy | turns an amount + inputs into a list of shares; validates them |
Expense | payer, amount, the computed splits |
BalanceSheet | pairwise balances; updated per expense |
ExpenseService | entry point: builds expenses, updates balances |
The extension point is the split rule. A new rule, such as splitting by shares ("2 parts for me, 1 for you"), should be one new class with no edits elsewhere. Say this aloud; it's what interviewers are listening for.
Step 3: Data model for balances
Use a map of maps: balance[a][b] > 0 means a owes b that amount. On every expense paid by p, each participant u ≠ p with share s:
balance[u][p] += sbalance[p][u] -= s
Keeping both directions makes "show balances for u1" a single lookup.
Store money as integers in paise to avoid floating-point errors.
Step 4: The code (40 minutes)
import java.util.*;
interface SplitStrategy {
/** Returns each participant's share in paise; must sum to amount. */
Map<String, Long> split(long amount, List<String> users, List<Double> values);
}
class EqualSplit implements SplitStrategy {
public Map<String, Long> split(long amount, List<String> users, List<Double> values) {
long base = amount / users.size(), rem = amount % users.size();
Map<String, Long> out = new LinkedHashMap<>();
for (String u : users) out.put(u, base + (rem-- > 0 ? 1 : 0));
return out;
}
}
class ExactSplit implements SplitStrategy {
public Map<String, Long> split(long amount, List<String> users, List<Double> values) {
Map<String, Long> out = new LinkedHashMap<>();
long sum = 0;
for (int i = 0; i < users.size(); i++) {
long v = Math.round(values.get(i) * 100);
out.put(users.get(i), v);
sum += v;
}
if (sum != amount) throw new IllegalArgumentException("exact shares must add up to the total");
return out;
}
}
class PercentSplit implements SplitStrategy {
public Map<String, Long> split(long amount, List<String> users, List<Double> values) {
double pct = values.stream().mapToDouble(Double::doubleValue).sum();
if (Math.abs(pct - 100.0) > 1e-9) throw new IllegalArgumentException("percentages must add up to 100");
Map<String, Long> out = new LinkedHashMap<>();
long assigned = 0;
for (int i = 0; i < users.size(); i++) {
long v = (i == users.size() - 1) ? amount - assigned
: Math.round(amount * values.get(i) / 100.0);
out.put(users.get(i), v);
assigned += v;
}
return out;
}
}
class BalanceSheet {
private final Map<String, Map<String, Long>> owes = new TreeMap<>();
void record(String payer, Map<String, Long> shares) {
shares.forEach((user, share) -> {
if (user.equals(payer) || share == 0) return;
add(user, payer, share);
add(payer, user, -share);
});
}
private void add(String a, String b, long v) {
owes.computeIfAbsent(a, k -> new TreeMap<>()).merge(b, v, Long::sum);
}
List<String> lines(String onlyUser) {
List<String> out = new ArrayList<>();
owes.forEach((a, row) -> row.forEach((b, v) -> {
if (v > 0 && (onlyUser == null || onlyUser.equals(a) || onlyUser.equals(b)))
out.add(a + " owes " + b + ": " + String.format("%.2f", v / 100.0));
}));
return out.isEmpty() ? List.of("No balances") : out;
}
Map<String, Long> netPerUser() {
Map<String, Long> net = new TreeMap<>();
owes.forEach((a, row) -> row.forEach((b, v) -> net.merge(a, -v, Long::sum)));
return net; // positive = should receive, negative = should pay
}
}
class ExpenseService {
private final Map<String, SplitStrategy> strategies = Map.of(
"EQUAL", new EqualSplit(), "EXACT", new ExactSplit(), "PERCENT", new PercentSplit());
final BalanceSheet sheet = new BalanceSheet();
void addExpense(String payer, double amount, List<String> users, String type, List<Double> values) {
SplitStrategy s = strategies.get(type);
if (s == null) throw new IllegalArgumentException("unknown split type " + type);
if (users.isEmpty()) throw new IllegalArgumentException("no participants");
sheet.record(payer, s.split(Math.round(amount * 100), users, values));
}
}
public class Main {
public static void main(String[] args) {
ExpenseService svc = new ExpenseService();
svc.addExpense("u1", 1000, List.of("u1", "u2", "u3", "u4"), "EQUAL", List.of());
svc.addExpense("u1", 1250, List.of("u2", "u3"), "EXACT", List.of(370.0, 880.0));
svc.addExpense("u4", 1200, List.of("u1", "u2", "u3", "u4"), "PERCENT", List.of(40.0, 20.0, 20.0, 20.0));
svc.sheet.lines(null).forEach(System.out::println);
}
}Running it prints balances like u2 owes u1: 620.00 and u1 owes u4: 230.00. Walk the interviewer through one expense by hand to show the numbers are right.
Step 5: Edge cases (15 minutes)
- Unknown split type or user: fail with a clear message.
- Shares that don't add up: rejected by the strategy, in one place.
- Rounding: paise as integers; remainder handed out deterministically.
- Payer not among participants: valid. The payer just isn't charged.
- Zero or negative amounts: reject.
Follow-up: simplify debts
"Minimise the number of transactions to settle up" is the classic follow-up.
- Compute each user's net balance (sum of what they're owed minus what they owe).
- Put creditors in a max-heap and debtors in a max-heap by absolute amount.
- Repeatedly match the biggest debtor with the biggest creditor, settle the smaller of the two amounts, and push back any remainder.
static List<String> simplify(Map<String, Long> net) {
PriorityQueue<Map.Entry<String, Long>> cred = new PriorityQueue<>((x, y) -> Long.compare(y.getValue(), x.getValue()));
PriorityQueue<Map.Entry<String, Long>> debt = new PriorityQueue<>((x, y) -> Long.compare(y.getValue(), x.getValue()));
net.forEach((u, v) -> {
if (v > 0) cred.add(new AbstractMap.SimpleEntry<>(u, v));
else if (v < 0) debt.add(new AbstractMap.SimpleEntry<>(u, -v));
});
List<String> txns = new ArrayList<>();
while (!cred.isEmpty() && !debt.isEmpty()) {
var c = cred.poll(); var d = debt.poll();
long x = Math.min(c.getValue(), d.getValue());
txns.add(d.getKey() + " pays " + c.getKey() + ": " + String.format("%.2f", x / 100.0));
if (c.getValue() > x) cred.add(new AbstractMap.SimpleEntry<>(c.getKey(), c.getValue() - x));
if (d.getValue() > x) debt.add(new AbstractMap.SimpleEntry<>(d.getKey(), d.getValue() - x));
}
return txns;
}This greedy approach produces at most n − 1 transactions. Finding the true minimum is NP-hard in general, and saying so earns credit.
Machine coding round coming up?
InterviewPilot's machine-coding round focus keeps answers in this shape (requirements, entities, extension points, runnable code) for the whole session, in an overlay that Zoom, Meet and Teams shares don't capture.
Other follow-up questions to expect
- Groups: add a
Groupholding members and its ownBalanceSheet; expenses belong to a group. - Split by shares: a new
ShareSplitstrategy. Show how it plugs in without other changes. - Concurrency: two expenses updating the same balance. Synchronise
record, or use per-pair locks. - Persistence: put repositories behind interfaces; the service doesn't change.
- Expense history and edits: store expenses; editing means reversing the old splits, then applying the new ones.
What interviewers mark you down for
- Floating-point money.
- Split logic inside
ExpenseServicebehind if/else on the type. - No validation that shares match the total.
- No runnable demo.
Related
- Low-level design and machine coding guide
- Design a parking lot
- Design BookMyShow
- The coding interview guide for India
Frequently asked questions
Was this article useful?
Bring backup into your next round.
Install InterviewPilot, upload your resume, and walk in with structured answers a keystroke away — visible to you and nobody else.
More from the blog
The Coding Interview Guide for Software Engineers in India (2026)
Everything a software engineer in India needs for coding rounds: how online assessments, DSA rounds, machine coding and CS fundamentals differ by company type, a 10-week preparation plan, and how to perform in the room.
Design a Ride-Hailing App (Uber/Ola): System Design Interview Walkthrough
How to answer the ride-hailing system design question: location tracking, geospatial indexing, driver matching, trip state machine, pricing, ETA and the scaling issues to name.
Low-Level Design and Machine Coding Round: A Practical Guide
How to approach a machine coding or low-level design interview: a time-boxed workflow, the object-oriented principles interviewers check, a worked parking-lot example, common problems, and mistakes that lose points.
Discussion
No comments yet. Been through an interview like this? Your experience could help the next candidate.