Kotlin Has a Visibility Level Java Doesn't: internal
Java has four visibility levels, but only three keywords — the fourth, package-private, is what you get by saying nothing. Kotlin also has four, with a different lineup: public, private, protected, and a new one, internal. The defaults differ too, and so does the file structure they live in. None of it is hard, but a Java developer’s instincts are slightly off, so it’s worth a quick reset.
This post follows destructuring. It’s about who gets to see what.
public is the default
In Java, a declaration with no modifier is package-private. In Kotlin, a declaration with no modifier is public — visible everywhere. You almost never type the word public; it’s what you get for free:
class Service // public
fun helper() { } // public
The flip is deliberate: most things you write are meant to be used, so the common case is the quiet one. You add a modifier to restrict, not to expose.
private and protected
private means visible inside its enclosing scope — for a class member, the class; for a top-level declaration, the file:
class Account {
private var balance = 0 // visible only inside Account
}
private fun audit() { } // visible only in this file
protected works as in Java for class members — visible to the class and its subclasses — with one simplification: it does not also leak to the same package the way Java’s protected does. There’s no package-private level for it to blend into.
internal: visible within the module
This is the one with no Java equivalent. internal means visible everywhere within the same module — a set of files compiled together, like a Gradle source set — and invisible outside it:
internal class Engine // usable across the module, hidden from consumers
It fills a real gap. When you publish a library, public is your API and private is per-file; neither lets you share a class freely across your own code while keeping it out of the public surface. internal is exactly that — your module’s “public to us, private to them.” If you write libraries, you’ll use it constantly; in an app module, less so.
Files don’t dictate structure
Java ties you to one public class per file, named after the file. Kotlin drops that rule entirely. A single .kt file can hold any number of classes, functions, and properties at the top level:
// Geometry.kt
class Point(val x: Int, val y: Int)
class Line(val from: Point, val to: Point)
fun distance(a: Point, b: Point): Double = /* ... */
And the package is declared with a package line at the top — it does not have to mirror the directory layout the way Java enforces. Convention still suggests matching them for sanity, but the compiler doesn’t require it.
The practical upshot: group small, related declarations by topic into one file instead of scattering them across a directory of single-class files. A file named Geometry.kt holding a few related shapes and helpers is idiomatic, not a code smell.
Different visibility for a getter and setter
A common need: a property the world can read but only the class can change. Kotlin lets the setter carry its own, stricter modifier:
class Counter {
var value: Int = 0
private set // public to read, private to write
fun increment() { value++ }
}
Callers see counter.value but can’t assign to it — no separate backing field or hand-written getter required.
Restricting constructors
Marking a constructor private funnels creation through a factory, which is how you enforce validation or hand back a cached instance:
class Email private constructor(val address: String) {
companion object {
fun of(raw: String): Email? =
if ("@" in raw) Email(raw) else null
}
}
Email.of("a@b.com") // Email?, validated
// Email("nonsense") // won't compile — constructor is private
protected in practice
protected opens a member to subclasses but no one else — useful for a hook that extenders override or call, while staying invisible to ordinary callers:
abstract class Repository {
protected abstract fun connect(): Connection // for subclasses only
fun query(sql: String): Result {
val c = connect()
return c.run(sql)
}
}
A worked example
One class can use every level at once, each chosen by who needs to see the member:
open class BankAccount internal constructor( // created only within the module
val id: String, // public: part of the contract
) {
var balance: Long = 0
private set // read anywhere, write only here
internal fun audit(): Long = balance // module tooling can peek
protected open fun onOverdraft() { } // subclasses can react
private fun applyFee() { balance -= 25 } // pure internal mechanism
}
Reading the modifiers tells you the intended audience of each member at a glance — which is the entire point of having four of them.
Final thoughts
Two shifts to absorb: public is the default, so you annotate to hide rather than to reveal; and internal gives you a module-wide scope that Java never had, which is the right tool for a library’s shared-but-not-exported guts. Pair that with files that can hold whatever set of declarations belong together, and you organize code by what it’s about, not by a one-type-per-file rule.
Next, the by keyword and the pattern it unlocks: handing off work to another object without the boilerplate. That’s delegation.
Practice: reinforce this with the companion workbook — short, click-to-reveal problems.