Skip to content
Preptima
mediumCodingDesignMidSeniorStaff

Build a shared-expense tracker. Three people split a bill of ten pounds evenly - write the classes, and tell me where the missing penny goes.

The interesting part is not the feature list, it is one invariant: the shares of an expense must sum to the expense total exactly, in every currency, forever. That forces money into integer minor units and forces a stated rule for allocating the remainder.

6 min readUpdated 2026-07-29Target archetype: Big Tech, Enterprise Captive, Product Startup
Practice answering out loud

What the interviewer is scoring

  • Whether money is held in minor units or an exact decimal type, with the reason given
  • Does the candidate state the sum-to-total invariant and enforce it in one place
  • That the remainder is allocated by a deterministic stated rule rather than rounded away
  • Whether balances are derived from the expense history instead of stored and mutated
  • Does the candidate scope the ninety minutes and say what is deliberately left out

Answer

Decide what you are building in the first ten minutes

Ninety minutes buys a working core, not a product, and the marks come from choosing the core deliberately. State the scope out loud: expenses with participants and a split strategy, a derived balance per participant, and a settlement suggestion. In-memory storage behind a repository interface. A main that runs one scenario end to end and prints balances. No authentication, no persistence, no HTTP, no groups-within-groups.

Then say what you are protecting. Every requirement in this problem reduces to one invariant: the shares of an expense sum to the expense total, exactly. If that ever fails by a penny, money appears or vanishes, balances stop reconciling, and no amount of feature work rescues it. Naming that invariant before writing a class is the single strongest opening in this round, because everything you do afterwards can be justified against it.

Money is not a double

double cannot represent 0.10 exactly, because it is binary floating point and one tenth is not a finite binary fraction. So repeated addition drifts, equality comparisons fail unpredictably, and a total that should be 100.00 prints as 99.99999999999999. This is not a rounding preference, it is the wrong type for the job, and using it is the fastest way to fail this round.

Hold money as an integer count of minor units — pence, cents — inside a value object that also carries the currency. An exact decimal type with an explicitly set scale and rounding mode is the other acceptable answer, and it is preferable when the domain has sub-penny prices such as per-unit rates. Either way the type must refuse to add two amounts in different currencies, because that operation has no meaning without a rate and a date, and silently permitting it is how a multi-currency bug becomes unfindable.

public record Money(long minorUnits, Currency currency) {

    public Money plus(Money other) {
        // Refuse rather than convert. A conversion needs a rate and a date,
        // neither of which this type has any business inventing.
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException("cannot add " + currency + " to " + other.currency);
        }
        return new Money(minorUnits + other.minorUnits, currency);
    }
}

Where the penny goes

Ten pounds between three people is 1000 pence divided by three: 333 each, with one penny left over. The wrong answers are to round each share to 333 and lose a penny, or to round each up to 334 and invent two. The right answer is to give the remainder to somebody, by a rule you state.

public static List<Money> splitEvenly(Money total, int ways) {
    if (ways <= 0) throw new IllegalArgumentException("ways must be positive");

    long base      = total.minorUnits() / ways;
    long remainder = Math.floorMod(total.minorUnits(), ways);  // non-negative even for refunds

    List<Money> shares = new ArrayList<>(ways);
    for (int i = 0; i < ways; i++) {
        // The first `remainder` participants pay one extra minor unit. This is
        // the line that keeps the shares summing back to the total exactly.
        shares.add(new Money(base + (i < remainder ? 1 : 0), total.currency()));
    }
    return shares;
}

Two details in there are worth pointing at while you write them. Math.floorMod rather than % matters because an expense can be negative — a refund, or a correction — and the % operator in Java returns a negative remainder for a negative left operand, which would hand out a share of base - 1 to nobody and break the sum. And the allocation order must be stable and defined, not "whatever the set iterates as", or the same bill split twice produces different shares.

Say what the rule is in business terms too, because an interviewer is listening for whether you noticed it is a fairness question and not only an arithmetic one. First-N-participants is fine for one bill and unfair across fifty if the order is always the same, so the natural extension is to rotate the starting position by a group-level counter, or to give the remainder to the payer since they are owed money anyway.

The class boundary that matters

Only one class should be able to produce shares, and it should be impossible to construct an expense whose shares do not sum.

public final class Expense {
    private final Participant paidBy;
    private final Money total;
    private final Map<Participant, Money> shares;

    public Expense(Participant paidBy, Money total, SplitStrategy strategy,
                   List<Participant> participants) {
        this.shares = strategy.split(total, participants);
        Money sum = shares.values().stream().reduce(new Money(0, total.currency()), Money::plus);
        // The invariant, checked at the boundary. Nothing downstream has to
        // trust the strategy, and a new strategy cannot break the ledger.
        if (!sum.equals(total)) throw new IllegalStateException("shares do not sum to total");
        this.paidBy = paidBy;
        this.total  = total;
    }
}

That check is what lets you add strategies freely. EqualSplit, ExactAmounts and Percentage all have their own way of being wrong — percentages that sum to 99, exact amounts a user mistyped — and all of them are caught by one assertion rather than by three sets of validation. It is also the natural place for a property-based test: for any total and any participant count, the shares sum to the total.

Balances are derived, not stored

The next decision people get wrong is keeping a running balance per participant and updating it on each expense. Two writes then have to agree forever, an edited or deleted expense leaves the balance wrong, and a rounding discrepancy has nowhere to be discovered.

Treat the expense list as the source of truth and compute net balances from it: for each expense, the payer is owed the total minus their own share, and everybody else owes their share. A balance is then a pure function of history, which makes it correct by construction, makes an edit trivially handled by recomputation, and gives you a reconciliation check you can run at any time — the net balances of a group must sum to zero. If performance ever demands a cached balance, cache it as a materialised view with that zero-sum check as the guard, not as the primary record.

Settlement is the last piece and the one to keep humble. The usual approach is greedy: net everybody out, then repeatedly match the largest debtor against the largest creditor. It produces few transfers and it is a heuristic, not a guaranteed minimum — minimising transfers exactly is a hard combinatorial problem. Saying that plainly is better than claiming optimality, and it is a distinction interviewers specifically listen for on this question.

What to leave visibly unfinished

With time remaining, spend it on tests for the money invariant rather than on features, and leave the seams obvious: a repository interface with a map behind it, a SplitStrategy interface with three implementations, no framework anywhere. Then say what you would do next and why you did not — persistence, concurrent edits to the same group, currency conversion with a stored rate. A candidate who explains a deliberate boundary reads as senior; one whose code simply stops reads as out of time.

Write down the invariant that shares sum to the total, then choose every type and every boundary to make violating it impossible. The missing penny is not a rounding detail, it is the requirement in miniature.

Likely follow-ups

  • The same three people split fifty bills. How do you stop one of them always paying the extra penny?
  • Somebody edits an expense from last month. What happens to the balances, and to a settlement already made?
  • Two participants are in different currencies. What do you store on the expense, and what do you refuse to compute?
  • Your settlement suggests four transfers and a participant finds three. Is your algorithm wrong?

Related questions

machine-codingmoneyvalue-objectsinvariantsexpense-splitting