RizTech Academy logo
RizTech Academy
Capstone: The Trip SplitterLesson 3 of 545 min

The settlement logic with collections and functions

This is the heart of the capstone: the two functions that turn a pile of expenses into "who pays whom". They are built almost entirely from the collections and functions modules — no elaborate classes, just data flowing through transformations. Every result here was produced by running the real code.

Step one: net balances

Before you can say who pays whom, you need to know, for each person, whether they are up or down across all the expenses. A person who paid for a lot is owed money (positive); a person who paid for little is owes money (negative). That is the net balance:

/**
 * Positive means the trip owes them; negative means they owe the trip.
 */
fun netBalances(expenses: List<Expense>): Map<Person, Int> {
    val balances = mutableMapOf<Person, Int>()
    for (e in expenses) {
        // the payer is credited the full amount they paid
        balances[e.paidBy] = (balances[e.paidBy] ?: 0) + e.amountPaise
        // every participant is debited their equal share
        for (p in e.sharedBetween) {
            balances[p] = (balances[p] ?: 0) - e.sharePaise
        }
    }
    return balances
}

Read the logic: for each expense, the payer gets credited the whole amount (they laid it out), and everyone who shared it gets debited their equal share (that is what they consumed). Note balances[e.paidBy] ?: 0 — a map lookup returns null for a key not yet present (the collections module), and ?: 0 supplies the starting balance (the null-safety module). Two lessons, one idiom, in every line.

Run it on the Goa trip — Kavita paid ₹9000, Ravi ₹3000, Neha ₹1500, all shared three ways:

Kavita is owed ₹4500.00
Ravi owes ₹1500.00
Neha owes ₹3000.00

Check it by hand: the total is ₹13,500, split three ways is ₹4,500 each. Kavita paid ₹9,000 and owes ₹4,500, so she is up ₹4,500. Ravi paid ₹3,000 and owes ₹4,500, so he is down ₹1,500. Neha paid ₹1,500 and owes ₹4,500, so she is down ₹3,000. The numbers match — and notice the crucial invariant: +4500 - 1500 - 3000 = 0. The balances always sum to zero, because every rupee credited to a payer is debited from the sharers. That invariant is what makes a settlement possible, and it is the first thing we will test.

Step two: who pays whom

Now turn those balances into actual payments. The goal is the shortest list of transfers that settles everyone. The approach is a greedy algorithm: repeatedly match the person who owes the most to the person who is owed the most, and transfer as much as possible between them.

fun settle(expenses: List<Expense>): List<Transfer> {
    val balances = netBalances(expenses).toMutableMap()
    val transfers = mutableListOf<Transfer>()

    while (true) {
        val debtor = balances.minByOrNull { it.value } ?: break     // owes the most (most negative)
        val creditor = balances.maxByOrNull { it.value } ?: break   // owed the most (most positive)
        if (debtor.value >= 0 || creditor.value <= 0) break         // everyone settled

        val amount = minOf(-debtor.value, creditor.value)           // the most this pair can settle
        transfers.add(Transfer(debtor.key, creditor.key, amount))
        balances[debtor.key] = debtor.value + amount                // debtor pays some off
        balances[creditor.key] = creditor.value - amount            // creditor is paid some
    }
    return transfers
}

Read the algorithm:

  • minByOrNull { it.value } finds the biggest debtor (most negative balance); maxByOrNull finds the biggest creditor (most positive). These are the collection operations from the collections module, applied to the balance map.
  • if (debtor.value >= 0 || creditor.value <= 0) break — when the biggest debtor owes nothing and the biggest creditor is owed nothing, everyone is settled, and we stop.
  • minOf(-debtor.value, creditor.value) is the amount this pair can settle — the smaller of what the debtor owes and what the creditor is owed. One of them is now fully settled after this transfer.
  • We record the transfer and update both balances, then loop.

Because each iteration fully settles at least one person, the loop terminates quickly and produces a short list. Run it on the Goa trip:

Neha pays Kavita ₹3000.00
Ravi pays Kavita ₹1500.00

Two transfers, and check them: Neha owed ₹3,000, Ravi owed ₹1,500, and Kavita was owed ₹4,500 — ₹3,000 + ₹1,500 = ₹4,500. Everyone is even. The tangle of "who paid for what" collapses to two clean payments, which is exactly what the brief asked for.

Why it is written this way

Step back and notice the shape. The logic is:

  • A Map<Person, Int> as the core data structure — the right structure (the collections module) for "a value per person", with fast lookup.
  • minByOrNull / maxByOrNull — collection operations, not hand-rolled loops to find the extremes.
  • ?: 0 and ?: break — null handled inline at the point of use (the null-safety module); a missing balance defaults to zero, an empty map breaks the loop cleanly.
  • Small, named functions — netBalances does one thing, settle does one thing, each nameable for it (the best-practices module).

There is no elaborate class hierarchy, no pattern, no ceremony — just data flowing through transformations, which is the collections module's whole promise. That is what good Kotlin looks like: the data and its transformations are the design, and they read like the explanation of the algorithm.

formatRupees — the finishing touch

One small helper for display, and it is where money does meet a decimal — but only for showing, never for calculating:

fun formatRupees(paise: Int): String = "₹%.2f".format(paise / 100.0)

900000 becomes "₹9000.00". The paise / 100.0 produces a Double only at the display boundary, where a rounding error of a fraction of a paisa cannot affect any calculation — all the arithmetic happened in exact integer paise. This is the discipline: compute in integer minor units, convert to a decimal only to display. It is the same boundary principle from null safety and error handling, applied to money.

Check your work

What a net balance is. Per person, the amount they are owed (positive) or owe (negative) across all expenses.

How netBalances computes it. Credits the payer the full amount, debits each sharer their equal share, over every expense.

The idiom in balances[key] ?: 0. A map lookup returns null for a missing key; ?: 0 supplies the starting value.

The invariant balances always satisfy. They sum to zero — every credit is matched by debits.

The settlement approach. Greedy — repeatedly match the biggest debtor to the biggest creditor and transfer as much as possible.

What minByOrNull/maxByOrNull find here. The biggest debtor (most negative) and biggest creditor (most positive).

What minOf(-debtor.value, creditor.value) is. The most this pair can settle — after it, one of them is fully settled.

Why the loop terminates quickly. Each iteration fully settles at least one person.

Where money becomes a Double. Only in formatRupees, at the display boundary — never in the arithmetic.

Practice

  1. Write netBalances and run it on the Goa trip. Confirm the three balances and that they sum to zero.
  2. Add a fourth person who paid nothing but shared everything. Predict their balance, then check.
  3. Confirm balances[somePerson] ?: 0 handles a person absent from the map without crashing.
  4. Write settle and run it on the Goa trip. Confirm the two transfers and that they clear Kavita's ₹4,500.
  5. Trace the greedy loop by hand for the Goa trip: what are the balances after each transfer?
  6. Construct a case where three transfers are needed and confirm settle produces them.
  7. Confirm formatRupees(900000) is "₹9000.00", and explain why the Double there is safe but a Double in netBalances would not be.
  8. Run settle(emptyList()) and confirm it returns an empty list without error.

Official documentation

Next: a command-line interface that reads like Kotlin.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship