RizTech Academy logo
RizTech Academy
Writing Idiomatic KotlinLesson 2 of 525 min

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 stock becomes checkStock(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.kt in every project is a filing cabinet with drawers labelled "paper". Prefer order.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 Order and its OrderStatus enum belong together, not in two files.
  • Top-level functions over a Utils class. 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

  1. Take the four-altitude placeOrder and extract each concern into its own well-named function. Confirm the top function reads as a summary.
  2. Find a block in your code that has a comment explaining it, and turn the comment into a function name.
  3. Find a function with four or more parameters. Either group some into a data class or justify each.
  4. Find a boolean parameter and either name it at the call site or split the function in two.
  5. Flatten an arrow-shaped function with guard clauses. Count the else branches you removed.
  6. Find a Utils.kt or Helpers.kt and decide where each function actually belongs by feature.
  7. Take the longest function you have written and list the "things" it does. Extract until each does one.

Official documentation

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