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/kotlinfor production code,src/test/kotlinfor tests. Gradle finds them by location.- The test directory mirrors the main directory.
SettlementTest.ktsits in the same package (splitter) as theSettlement.ktit 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).
One file, several related types
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
- Look at the Trip Splitter layout and identify the package, the main/test split, and the mirroring.
- Add a package declaration to a file and confirm it must match the directory.
- Take a
utils.kt(real or imagined) and redistribute its functions to feature-named files. - 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.
- Put two related small classes in one file and two unrelated ones in another. Justify each grouping.
- Take a class and make its implementation-detail functions
private. Confirm the public surface shrinks and nothing outside breaks. - Explain, in one sentence, why "expose the minimum" makes code easier to change.
Official documentation
- Kotlin — Packages and imports — Declaring packages and how they map to directories.
- Kotlin — Visibility modifiers —
public,private,internal,protected. - Kotlin — Coding conventions: source file organization — Multiple declarations per file, and layout.
- Android — Guide to app architecture — How larger projects organise by feature and layer.
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