RizTech Academy logo
RizTech Academy
Testing and ToolingLesson 3 of 320 min

Structuring a Kotlin project

A single file is fine for learning. A real project is many files, and where things live determines whether the codebase is a pleasure or a maze. This lesson is how to structure a Kotlin project — the directory layout, packages, and the organising principle — using the Trip Splitter capstone as the worked example. It is short, because the rules are few and the principle is one you already know.

The standard layout

Every Gradle Kotlin project shares the layout from the Gradle lesson:

trip-splitter/
├── build.gradle.kts
├── settings.gradle.kts
└── src/
    ├── main/kotlin/splitter/     production code, in a package
    │   ├── Model.kt              Person, Expense, Transfer
    │   ├── Settlement.kt         netBalances, settle, formatRupees
    │   └── Main.kt               the entry point
    └── test/kotlin/splitter/     tests, mirroring the main structure
        └── SettlementTest.kt

Two conventions to internalise:

  • src/main/kotlin for production code, src/test/kotlin for tests. Gradle finds them by location.
  • The test directory mirrors the main directory. SettlementTest.kt sits in the same package (splitter) as the Settlement.kt it tests. This mirroring means the test for any file is exactly where you would look for it — a small convention with a big payoff in navigability.

Packages

A package is a namespace — a way to group related code and give it a full name. You declare it at the top of each file:

package splitter

data class Person(val name: String)

The package should match the directory: a file in src/main/kotlin/splitter/ declares package splitter. For a larger app, packages nest — com.riztech.tripsplitter.model, com.riztech.tripsplitter.ui — and the directories nest to match.

The convention for the package name is a reversed domain — com.riztech.tripsplitter — which guarantees it is globally unique (no two companies own riztech.com). You will see this in every Android project. For a small learning project, a single simple package like splitter is fine; the reversed-domain form matters when your code might mix with other people's.

Organise by feature, not by kind

Here is the principle that matters most, and it is the one from the best-practices module applied to directories. There are two ways to group files, and one is better:

# by KIND — tempting, and wrong for anything real
splitter/
├── models/          Person, Expense, Transfer, Booking, User, ...
├── services/        SettlementService, BookingService, ...
└── utils/           everything else

# by FEATURE — how real projects grow
splitter/
├── expense/         Expense, ExpenseValidation, ExpenseTest
├── settlement/      Settlement, Transfer, SettlementTest
└── person/          Person

Grouping by kind (all models together, all services together) looks tidy at first and becomes painful: a change to how expenses work touches models/, services/, and utils/ — three directories for one conceptual change. Grouping by feature keeps everything about expenses in one place, so a feature change touches one directory. As a project grows, by-feature scales; by-kind turns into the "filing cabinet with drawers labelled paper" from the naming lesson.

For the small capstone, a couple of files in one splitter package is right — you do not need feature folders for six files. But know the principle for when a project grows: group by what the code is about, not by what kind of thing it is. And never create a utils or helpers package — it is where unrelated code goes to be forgotten. Name the file for its feature (money.kt, not utils.kt).

Kotlin does not force one class per file (unlike Java). Small, related types should share a file. The capstone's Model.kt holds Person, Expense, and Transfer together, because they are the domain's vocabulary and are always read together. Splitting them into three tiny files would add navigation for no benefit.

The rule: a file should hold one coherent idea, which might be one big class or several small related ones. Settlement.kt holds the settlement functions; Model.kt holds the data types. Each file has one reason to change — the same "one thing" principle as functions, at the file level. When a file grows to hold two unrelated ideas, split it; until then, keeping related types together aids the reader.

Public versus internal — controlling what is exposed

As a project grows, you control what each part exposes with visibility modifiers:

class Settlement {
    fun settle(...) = ...           // public (the default) — anyone can call it
    private fun matchDebts(...) =   // private — only this class
    internal fun helper(...) =      // internal — anyone in this module, but not outside it
}
  • public (the default) — visible everywhere. Do not make everything public by reflex; a smaller public surface is easier to change safely.
  • private — visible only within the file or class. The default for anything that is an implementation detail.
  • internal — visible within the same module (roughly, the same Gradle project) but not to code that depends on it. Useful for a library's internals.

The habit: expose the minimum. Make a function or property private unless something outside genuinely needs it. A small, deliberate public API is easier to understand, safer to change, and signals clearly what is meant to be used versus what is internal machinery. This is the encapsulation idea — a class or module should hide how it works and expose only what it does.

The whole idea

Project structure is the "one thing, nameable for it" principle scaled up from functions to files to packages: each file does one coherent thing, related files group by feature, and each part exposes the minimum. A codebase organised this way is one you can navigate on your first day — you know where things live because they live where they belong. That navigability is worth more than it sounds, and it is exactly what the capstone, which you build next, is structured to demonstrate.

Check your work

The standard layout. src/main/kotlin for production code, src/test/kotlin for tests, with the test directory mirroring the main one.

What a package is, and how it relates to directories. A namespace grouping related code; the package matches the directory path.

The package-name convention. A reversed domain (com.riztech.app) for global uniqueness.

Organise by feature, not by kind. Group files by what they are about (expense, settlement), so a feature change touches one directory — not models/, services/, utils/.

Why never a utils package. It is where unrelated code goes to be forgotten; name for the feature.

Kotlin's rule on files and classes. Not one class per file — small, related types should share a file that holds one coherent idea.

The three visibility modifiers. public (default, everywhere), private (this file/class), internal (this module).

The visibility habit. Expose the minimum — private by default; a small public surface is safer to change.

The unifying principle. Each file does one coherent thing, files group by feature, each part exposes the minimum.

Practice

  1. Look at the Trip Splitter layout and identify the package, the main/test split, and the mirroring.
  2. Add a package declaration to a file and confirm it must match the directory.
  3. Take a utils.kt (real or imagined) and redistribute its functions to feature-named files.
  4. Sketch two folder structures for an app you know — one by kind, one by feature — and reason about which change would be easier in each.
  5. Put two related small classes in one file and two unrelated ones in another. Justify each grouping.
  6. Take a class and make its implementation-detail functions private. Confirm the public surface shrinks and nothing outside breaks.
  7. Explain, in one sentence, why "expose the minimum" makes code easier to change.

Official documentation

Next module — the Capstone: design, build, and test a real Kotlin program end to end.

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