Function size, extraction and file organisation
Every rule about function size is somebody's guess dressed as a law. "Functions under twenty lines." "Files under four hundred." They are usually right, and the number is never the reason. The reason is: a function should do one thing, at one level of abstraction. Size is a symptom.
One thing, one level of abstraction
Here is a function that is not long, but is hard to read:
fun placeOrder(customerId: Int, itemId: Int, qty: Int) {
val customer = customers.find { it.id == customerId }
?: throw IllegalArgumentException("no customer $customerId")
val item = items.find { it.id == itemId }
?: throw IllegalArgumentException("no item $itemId")
if (item.stock < qty) throw IllegalStateException("not enough stock")
val total = item.pricePaise * qty
val discounted = if (customer.isPremium) (total * 90) / 100 else total
item.stock -= qty
orders.add(Order(customerId, itemId, qty, discounted))
println("Order placed: ₹${discounted / 100}")
}
It works, but four different altitudes are stacked in it: looking things up, checking a business rule, calculating a price, and recording the result. A reader wanting the pricing logic has to wade through lookups and stock checks to find it. Extract each concern to its own level:
fun placeOrder(customerId: Int, itemId: Int, qty: Int) {
val customer = findCustomer(customerId)
val item = findItem(itemId)
checkStock(item, qty)
val total = priceFor(item, qty, customer)
recordOrder(customer, item, qty, total)
}
Same work. Now the function reads as a summary — find, check, price, record — and each detail is one step down if you want it. The test: can you read the function and understand what it does without reading anything it calls? The second version passes; the first does not.
What to extract, and what to leave
Extracting is not free — a name to invent, and a jump for the reader. Extract when:
- The block needs a comment to say what it does. The comment is the function name you have not
written yet.
// check there is enough stockbecomescheckStock(item, qty). - It is at a different altitude from its neighbours (lookups amid business logic).
- It is duplicated, or nearly.
- You want to test it separately.
Leave it inline when it is used once, is three obvious lines, or when extracting would need four
parameters to carry the context. If you cannot name the extracted function better than the code it
replaces, the extraction earns nothing — doTheThing(x) is worse than the three lines it hides.
Parameters: fewer is better
Zero is best, one is good, two is fine, three is a smell, four means something is wrong. A long parameter list is hard to call correctly and usually means the function is doing too much, or that some of those parameters belong together in an object.
Kotlin gives you tools the functions module covered — default arguments to shrink the list, and named arguments to make a longer call readable:
// hard to call correctly
createUser("Kavita", "k@x.com", true, false, 3)
// named arguments rescue the call site
createUser(name = "Kavita", email = "k@x.com", isAdmin = true, isVerified = false, maxSessions = 3)
But the deeper fix for too many parameters is often to group related ones into a data class. If
street, city, pincode, and state always travel together, they are an Address, and passing
one Address beats passing four strings — it cannot be got out of order, and it names the concept.
A boolean parameter is nearly always worth a second look. save(entry, true) tells the reader
nothing; either name it (save(entry, notify = true)) or split into two clearly-named functions
(saveAndNotify, saveQuietly). The best-practices instinct is to make the call site
self-explanatory.
Return early to stay flat
Deeply nested code is hard to follow. Guard clauses — handle the exceptional cases first and return — keep the main logic flat and unindented:
// arrow-shaped: the happy path is buried deepest
fun priceFor(item: Item?, qty: Int): Int {
if (item != null) {
if (qty > 0) {
if (item.stock >= qty) {
return item.pricePaise * qty
} else throw IllegalStateException("not enough stock")
} else throw IllegalArgumentException("qty must be positive")
} else throw IllegalArgumentException("no item")
}
// flat: guards at the top, the real work at the bottom, unindented
fun priceFor(item: Item?, qty: Int): Int {
if (item == null) throw IllegalArgumentException("no item")
if (qty <= 0) throw IllegalArgumentException("qty must be positive")
if (item.stock < qty) throw IllegalStateException("not enough stock")
return item.pricePaise * qty
}
Same behaviour. The second reads top to bottom: here are the conditions that stop us, and here is the
work. Every else is gone — and each else was a place to make a mistake. Kotlin's smart casts make
this even nicer: after if (item == null) throw ..., the compiler knows item is non-null below, so
the rest is clean. Guards at the top, work at the bottom, no arrow.
Organising files
The same "one thing" principle scales to files:
- A file should have one reason to change. Group by feature, not by kind. A
models.kt,utils.kt,helpers.ktin every project is a filing cabinet with drawers labelled "paper". Preferorder.kt,pricing.kt,customer.kt— so a change to pricing touches one file. - Kotlin lets several classes live in one file, and small related types often should — an
Orderand itsOrderStatusenum belong together, not in two files. - Top-level functions over a
Utilsclass. If a function does not belong to a class, it is a top-level function in a well-named file (money.kt), not a static method on a pointless holder. The Java habit of wrapping everything in a class is one to drop.
The unifying idea across names, functions, and files is the same: each unit should do one thing and be nameable for it. When you cannot name a function, a class, or a file cleanly, it is usually because it does more than one thing — and the fix is to split it, not to invent a vaguer name.
Check your work
The real rule behind size limits. A function should do one thing at one level of abstraction; size is a symptom.
The readability test for a function. Can you understand it without reading what it calls?
Four reasons to extract. It needs a comment, it is a different altitude, it is duplicated, or you want to test it separately.
When not to extract. Used once, three obvious lines, or you cannot name it better than the code.
The parameter-count guidance, and two fixes. Three is a smell; use named/default arguments, and group related parameters into a data class.
Why a boolean parameter deserves a second look. save(x, true) is opaque — name it or split into
two functions.
What guard clauses buy. The happy path stays flat and unindented; every else (a place for bugs)
disappears.
How to organise files. By feature, not kind; several related classes per file; top-level
functions over a Utils class.
The unifying idea. Each unit does one thing and is nameable for it; if you cannot name it cleanly, it does too much.
Practice
- Take the four-altitude
placeOrderand extract each concern into its own well-named function. Confirm the top function reads as a summary. - Find a block in your code that has a comment explaining it, and turn the comment into a function name.
- Find a function with four or more parameters. Either group some into a data class or justify each.
- Find a boolean parameter and either name it at the call site or split the function in two.
- Flatten an arrow-shaped function with guard clauses. Count the
elsebranches you removed. - Find a
Utils.ktorHelpers.ktand decide where each function actually belongs by feature. - Take the longest function you have written and list the "things" it does. Extract until each does one.
Official documentation
- Kotlin — Coding conventions: functions — Formatting, expression bodies, and defaults.
- Kotlin — Coding conventions: source file organization — Grouping by feature and multiple declarations per file.
- Kotlin — Returns and jumps — Early returns and labels for guard clauses.
Next: the scope functions and the idioms that mark a Kotlin developer.
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