Scope and packages
Kotlin’s organisational structure is file-based packages with explicit access control. Each .kt file may declare a package; the file’s contents share that namespace with other files declaring the same package. Top-level declarations (functions, properties, classes) are admitted — Kotlin does not require everything to be inside a class. The conventional access modifiers — public (default), internal (module-only), protected, private — admit substantial visibility control. Imports admit referring to declarations from other packages, with optional aliasing via as. The conventional contemporary Kotlin project uses Gradle with the Kotlin Multiplatform plugin (or the JVM-only Kotlin plugin) for build organisation. The combination — file-based packages, four visibility modifiers, top-level declarations, the import grammar with aliasing, the Gradle-based module system — is the substance of Kotlin’s organisational model.
Packages
Each file may declare a package:
package com.example.app
fun greet(name: String) = "Hello, $name"
class Person(val name: String, val age: Int)
The file name and directory structure are not required to match the package — the package declaration is authoritative. Conventional discipline matches the package and directory:
src/main/kotlin/
└── com/example/app/
├── Main.kt (package com.example.app)
├── User.kt (package com.example.app)
└── service/
└── UserService.kt (package com.example.app.service)
The conventional discipline:
- Match directory structure to package.
- Use lowercase package names.
- Use reverse-domain notation (
com.company.product).
Imports
import java.util.Date
import kotlin.math.PI
import kotlin.math.sqrt
import com.example.lib.User
import com.example.lib.UserService.findById // import a member
import com.example.lib.User as LibUser // alias
import com.example.lib.* as LibAll // wildcard alias (rare)
import com.example.lib.* // wildcard
The Kotlin standard library imports several packages automatically:
kotlin.*kotlin.annotation.*kotlin.collections.*kotlin.comparisons.*kotlin.io.*kotlin.ranges.*kotlin.sequences.*kotlin.text.*- On JVM:
java.lang.*andkotlin.jvm.* - On JS:
kotlin.js.*
Most routine code requires no explicit imports for these.
Visibility modifiers
The four modifiers:
| Modifier | Visibility |
|---|---|
public (default) | Visible everywhere |
internal | Visible within the same module |
protected | Visible in the declaring class and subclasses |
private | Visible in the declaring class or file |
public class Foo { // visible everywhere (default)
public val name: String = "" // public (default)
internal var counter: Int = 0 // module-only
protected open fun helper() { } // class + subclasses
private val cache = mutableMapOf<String, Any>() // class only
}
internal class Helper { /* ... */ } // module-only
private fun fileHelper() { /* ... */ } // file-only (top-level)
For top-level declarations, private means “file-only”.
The default for class members and top-level declarations is public — admit conventional public APIs.
The conventional discipline:
- Default to
publicfor the public API. - Use
internalfor module-internal helpers. - Use
privatefor implementation details. - Use
protectedin class hierarchies.
Modules
A module in Kotlin is a unit of code compiled together — typically a Gradle module/subproject, a Maven module, or an IntelliJ module. The internal visibility is module-scoped.
For Gradle:
// build.gradle.kts
plugins {
kotlin("jvm") version "2.1.0"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
testImplementation(kotlin("test"))
}
Treated in Gradle and packaging.
Top-level declarations
Kotlin admits top-level declarations — functions, properties, classes outside any class:
package com.example.util
const val MAX_RETRIES = 3 // top-level constant
val defaultTimeout: Duration = 30.seconds // top-level property
fun greet(name: String) = "Hello, $name" // top-level function
class Person(val name: String, val age: Int) // top-level class
The mechanism distinguishes Kotlin from Java (which requires everything inside a class).
For Java interop, top-level declarations compile to a file-named class:
// File: Greetings.kt, package com.example
fun greet(name: String) = "Hello, $name"
// In Java:
String message = GreetingsKt.greet("Alice"); // calls the Kotlin top-level
The GreetingsKt class is the auto-generated synthetic class. To rename:
@file:JvmName("Greetings")
package com.example
fun greet(name: String) = "Hello, $name"
Now Java code calls Greetings.greet("Alice").
Local declarations
Functions and classes admit local declarations:
fun processItems(items: List<Item>) {
fun isValid(item: Item) = item.name.isNotEmpty() // local function
val helper = ItemHelper() // local instance
items.filter(::isValid).forEach { helper.process(it) }
}
Local functions and classes admit substantial encapsulation; conventional for substantial helpers used only within one function.
Object declarations
The object keyword admits a singleton — a class with exactly one instance:
object DatabaseConnection {
val url: String = "localhost:5432"
private var connections: Int = 0
fun connect(): Connection {
connections++
return /* ... */
}
}
DatabaseConnection.connect() // method call on the singleton
Treated in Data classes and objects.
Companion objects
Inside a class, the companion object admits members associated with the type rather than instances:
class User(val id: Int, val name: String) {
companion object {
const val MAX_NAME_LENGTH = 100
fun create(name: String) = User(generateId(), name)
private fun generateId(): Int = /* ... */
}
}
User.MAX_NAME_LENGTH // 100
User.create("Alice") // factory call
The companion object admits substantial Java-style “static” members.
Nested vs inner classes
class Outer {
private val name = "Outer"
class Nested { // nested — does NOT admit access to outer's instance
fun show() = println("nested")
}
inner class Inner { // inner — admits access to outer's instance
fun show() = println("inner of $name")
}
}
val n = Outer.Nested() // construct nested directly
val i = Outer().Inner() // requires outer instance
By default, nested classes are not inner — the inner keyword is required for substantial outer-instance access.
internal for libraries
The internal modifier admits substantial encapsulation in libraries:
// In a library module:
internal class Implementation { /* hidden from consumers */ }
internal fun helper() { /* hidden */ }
public class API { // exposed
internal val impl = Implementation() // internally accessible
}
External consumers of the library cannot reference Implementation or helper.
Common patterns
Module organisation
my-project/
├── settings.gradle.kts
├── build.gradle.kts # root project
├── app/
│ ├── build.gradle.kts
│ └── src/main/kotlin/
│ └── com/example/app/
│ └── Main.kt
└── lib/
├── build.gradle.kts
└── src/main/kotlin/
└── com/example/lib/
├── PublicApi.kt
└── internal/
└── Implementation.kt
// settings.gradle.kts
rootProject.name = "my-project"
include(":app", ":lib")
Public API surface
// File: PublicApi.kt
package com.example.lib
public class Client(
private val configuration: Configuration // hidden implementation
) {
public fun fetch(request: Request): Response {
return internalFetch(request)
}
private fun internalFetch(request: Request): Response {
// ...
}
}
Internal helpers
// File: Helpers.kt (in com.example.lib)
package com.example.lib
internal fun parseDate(s: String): LocalDate {
// visible only within the lib module
}
Top-level constants
// File: Constants.kt
package com.example.app
const val APP_NAME = "MyApp"
const val APP_VERSION = "1.0.0"
const val MAX_RETRIES = 3
val DEFAULT_HEADERS = mapOf(
"User-Agent" to "$APP_NAME/$APP_VERSION"
)
Companion object factory
class User private constructor(
val id: Int,
val name: String,
val email: String
) {
companion object {
fun create(name: String, email: String): User {
require(email.contains("@")) { "Invalid email" }
return User(generateId(), name, email)
}
private fun generateId(): Int = /* ... */
}
}
val user = User.create("Alice", "alice@b.c")
The pattern admits substantial validation in factory methods.
Top-level extension functions
// File: StringExtensions.kt
package com.example.util
fun String.shout(): String = this.uppercase() + "!"
fun String.isAllDigits(): Boolean = all { it.isDigit() }
// In another file:
import com.example.util.shout
"hello".shout() // "HELLO!"
Object for namespacing constants
object AppConfig {
const val HOST = "localhost"
const val PORT = 8080
const val DEBUG = true
object URLs {
const val API = "https://api.example.com"
const val DOCS = "https://docs.example.com"
}
}
AppConfig.HOST
AppConfig.URLs.API
The pattern admits substantial namespace organisation.
Aliased imports
import java.util.Date as JavaDate
import kotlinx.datetime.LocalDate
class Event(
val date: LocalDate, // kotlinx-datetime
val javaDate: JavaDate // legacy java.util.Date
)
Wildcard imports (use sparingly)
import kotlin.math.* // imports all of kotlin.math
fun area(r: Double) = PI * r * r // PI imported via wildcard
fun hypotenuse(a: Double, b: Double) = sqrt(a * a + b * b)
The conventional discipline avoids wildcards except for math, collections-style packages.
@file: annotations
@file:JvmName("Greetings")
@file:Suppress("UNUSED_PARAMETER")
package com.example
fun greet(name: String) = "Hello, $name"
The @file: annotations admit substantial file-level configuration.
Multiplatform expect/actual
For Kotlin Multiplatform:
// In commonMain:
expect class PlatformDate {
fun toIso(): String
}
// In jvmMain:
actual class PlatformDate(private val instant: java.time.Instant) {
actual fun toIso(): String = instant.toString()
}
// In jsMain:
actual class PlatformDate(private val date: kotlin.js.Date) {
actual fun toIso(): String = date.toISOString()
}
The mechanism admits substantial cross-platform code with platform-specific implementations.
A note on the conventional discipline
The contemporary Kotlin scope advice:
- Use packages matching directory structure.
- Use lowercase package names — reverse-domain notation.
- Default to
publicfor APIs. - Use
internalfor module-only code in libraries. - Use
privatefor implementation details. - Use top-level declarations — Kotlin admits substantial flexibility outside classes.
- Use
companion objectfor factory methods and type-level constants. - Use
objectfor singletons and namespaces. - Use
@file:JvmNameto control Java interop. - Use
import asto disambiguate or shorten. - Avoid wildcard imports — admit explicit imports.
The combination — file-based packages, the four visibility modifiers, top-level declarations, the companion object mechanism, the substantial Java interop, the Multiplatform expect/actual mechanism — is the substance of Kotlin’s organisational model. The discipline produces clear, well-encapsulated code with substantial flexibility for module-level reuse.