A command-line interface that reads like Kotlin
The model holds the data, the logic computes the settlement — now they need to be wired together into
a program somebody can run. This lesson writes the main function that assembles the trip and prints
a readable report, and it is where all the pieces connect. The output shown is from the real program.
The entry point
Every runnable Kotlin program starts at fun main() (the getting-started module). Here is the
capstone's, in full — read it top to bottom as the story of the program:
package splitter
fun main() {
val kavita = Person("Kavita")
val ravi = Person("Ravi")
val neha = Person("Neha")
val everyone = listOf(kavita, ravi, neha)
val expenses = listOf(
Expense("Hotel", paidBy = kavita, amountPaise = 900000, sharedBetween = everyone),
Expense("Petrol", paidBy = ravi, amountPaise = 300000, sharedBetween = everyone),
Expense("Dinner", paidBy = neha, amountPaise = 150000, sharedBetween = everyone),
)
printReport(expenses)
}
Notice the details that make it read well:
- Named arguments —
paidBy = kavita, amountPaise = 900000, sharedBetween = everyone. Constructing anExpensereads like a labelled form; nobody can mix up the payer and the amount (the functions module). This is exactly why named arguments mattered. valthroughout — nothing here changes, so nothing is avar(the val-by-default habit).- A read-only
listOffor the expenses and the people (the collections module). mainreads as a summary — set up the people, record the expenses, print the report — with the detail one function down. That is the "one thing, one level of abstraction" principle from best-practices.
In a fuller version the expenses would come from user input (readLine()); here they are a fixed trip
so the output is reproducible and the lesson stays about structure. Reading input is a small extension
you will do in the practice.
Printing a readable report
The report is its own function, so main stays a summary:
fun printReport(expenses: List<Expense>) {
println("=== Trip to Goa ===")
println("Expenses:")
for (e in expenses) {
println(" ${e.description}: ${formatRupees(e.amountPaise)} paid by ${e.paidBy.name}")
}
println("\nNet balances:")
for ((person, balance) in netBalances(expenses)) {
val state = if (balance >= 0) "is owed" else "owes"
println(" ${person.name} $state ${formatRupees(kotlin.math.abs(balance))}")
}
println("\nSettlement:")
val transfers = settle(expenses)
if (transfers.isEmpty()) {
println(" Everyone is settled up.")
} else {
for (t in transfers) {
println(" ${t.from.name} pays ${t.to.name} ${formatRupees(t.amountPaise)}")
}
}
}
The Kotlin idioms are worth pointing out, because they are the whole course showing up in one function:
- String templates —
"${e.description}: ${formatRupees(e.amountPaise)} paid by ${e.paidBy.name}"reads like the sentence it prints, no+concatenation (the strings module). - Destructuring —
for ((person, balance) in netBalances(expenses))unpacks each map entry into a named pair (the collections and data-classes modules). ifas an expression —val state = if (balance >= 0) "is owed" else "owes"assigns the result directly, novar(the conditions module).- The empty case handled —
if (transfers.isEmpty())prints a friendly message rather than an empty section. Handling the "nothing to do" case is the mark of a finished program, not a demo.
Running it
With the application plugin from the Gradle lesson pointing at splitter.MainKt:
./gradlew run
produces exactly:
=== Trip to Goa ===
Expenses:
Hotel: ₹9000.00 paid by Kavita
Petrol: ₹3000.00 paid by Ravi
Dinner: ₹1500.00 paid by Neha
Net balances:
Kavita is owed ₹4500.00
Ravi owes ₹1500.00
Neha owes ₹3000.00
Settlement:
Neha pays Kavita ₹3000.00
Ravi pays Kavita ₹1500.00
There it is — the whole program, from a list of who-paid-what to a clean, human-readable "here is who
pays whom". Everything above the Settlement line is context; the two lines under it are the answer
the friends actually wanted, and computing them was the point.
What this lesson demonstrates
The main and printReport are almost pure glue — they contain no clever logic, because the logic
lives in netBalances and settle where it belongs. That separation is deliberate and is the design:
- The model (
Model.kt) knows the data. - The logic (
Settlement.kt) knows the algorithm. - The CLI (
Main.kt) knows how to read input and print output, and nothing else.
Each file does one thing, nameable for it (the project-structure lesson). If you later replaced the
command line with an Android screen, you would rewrite only Main.kt — the model and the logic,
which are the hard and valuable parts, would not change at all. That is why the settlement was written
as plain functions on plain data, with no UI mixed in: it makes the core reusable and testable, which
the next lesson relies on.
Check your work
Where a Kotlin program starts. fun main().
Why named arguments in the Expense construction. They label each field, so payer and amount
cannot be confused — construction reads like a form.
Why main reads as a summary. The detail lives in printReport, one level down — one thing, one
level of abstraction.
The idioms in the report. String templates, destructuring of map entries, if as an expression,
and handling the empty case.
Why the empty settlement case is handled. A finished program handles "nothing to do", not just the happy path.
How the program is run. ./gradlew run, with the application plugin pointing at the main
class.
The separation the design achieves. Model knows the data, logic knows the algorithm, CLI knows input/output — each file one thing.
Why that separation matters. Replacing the CLI with an Android screen would change only Main.kt;
the model and logic (the valuable, testable parts) stay untouched.
Practice
- Write
mainandprintReportand run the program with./gradlew run. Confirm the output matches. - Add a fourth person and a fourth expense, re-run, and confirm the report and settlement update correctly.
- Make an expense shared between only some of the people and confirm the balances reflect it.
- Create a case where everyone is already even (each person pays their own share) and confirm the report prints "Everyone is settled up."
- Replace one hard-coded expense with values read from
readLine(), handling the nullable input with?:or a check. - Extract the "Expenses" printing into its own small function and confirm
printReportstill reads as a summary. - Change the CLI output format (for example, group the settlement by who-pays) without touching
Settlement.kt. Note that the logic did not need to change.
Official documentation
- Kotlin — Basic syntax: program entry point —
fun main. - Kotlin — Strings: templates — The report's formatting.
- Kotlin — Destructuring declarations — Unpacking map entries in the loop.
- Gradle — The application plugin — Running the program with
./gradlew run.
Next: testing it, and the bugs the tests find.
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