Skip to Content
Course content

23: Visibility Modifiers

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you're coming from Java, you probably have a mental habit of thinking about "package-private" visibility. You might assume that if you don't provide a modifier, or if you use internal, you're restricting access to just the files in that specific folder. I see this mistake constantly in code reviews.

The "Internal equals Package-Private" Myth

In Java, if you omit a modifier, the class or member is visible to everything in the same package. It's a common mistake to assume Kotlin's internal modifier does the same thing. Let's look at why that's wrong.

// File: com.app.payments.PaymentProcessor.kt
internal class PaymentGateway {
    fun processPayment() { 
        println("Processing...") 
    }
}

// File: com.app.utils.NetworkHelper.kt (Different package!)
fun setup() {
    val gateway = PaymentGateway() // This WORKS in Kotlin
    gateway.processPayment()
}

In the example above, PaymentGateway is in the payments package, but NetworkHelper is in the utils package. If this were Java's default visibility, this would throw a compiler error. But in Kotlin, it works perfectly. Why? Because internal doesn't care about packages; it cares about modules.

Thinking in Modules, not Folders

A module is a set of Kotlin files compiled together—think of a Gradle source set, a Maven project, or an IntelliJ module. When you mark something as internal, you're telling Kotlin: "Anyone inside this specific build artifact can use this, but if this code is ever imported as a library by another project, keep it hidden."

I personally love internal because it allows me to organize my code into clean packages without worrying about visibility barriers, while still ensuring that the "public API" I expose to the rest of the company (or the world) stays lean and intentional.

Tightening the Screws with Private and Protected

While internal handles the module boundary, you still need the finer tools for class-level encapsulation. Remember that in Kotlin, the default visibility is public. If you don't type anything, the whole world can see it.

  • private: This is the strictest. If you mark a function private inside a class, it's invisible even to other classes in the same file. If you mark a property private at the top level of a file, it's invisible to every other file in the module.
  • protected: This is strictly for inheritance. A protected member is visible to the class it's defined in and any subclasses. You won't find protected on top-level functions because, well, you can't inherit from a function.

Let's look at a more realistic scenario where we mix these to protect a sensitive operation:

open class BaseTransaction {
    // Only subclasses can access the raw transaction ID
    protected val transactionId = "TXN-12345"

    // Only this specific class can handle the encryption key
    private fun encryptPayload(data: String) = "encrypted_$data"

    // Anything in the same module can trigger a sync
    internal fun syncWithServer() {
        println("Syncing $transactionId...")
    }

    // Anyone using this library can execute the payment
    fun execute() {
        val secret = encryptPayload("amount=100")
        println("Executing payment with $secret")
    }
}

Notice the hierarchy of trust here. I trust the execute() method to be public, I trust my subclasses to see the transactionId, and I trust my module to handle syncWithServer(). But I trust absolutely no one with the encryptPayload logic except the class itself.




📋 Practical Task

Hardening the Banking Transaction API

You are building a banking module. Currently, the BankVault class is too permissive, exposing sensitive internal logic to the entire application. Your task is to apply the correct visibility modifiers to secure the class based on the following requirements:

  • The vaultKey should be completely hidden from everything except the BankVault class.
  • The auditLog() method should be accessible to any class within the same Gradle module, but hidden if this module is imported as a library.
  • The calculateInterest() method should be accessible to the BankVault class and any classes that inherit from it (e.g., SavingsVault).
  • The deposit() method should remain accessible to the entire application.
class BankVault {
    val vaultKey = "SECRET_123"
    
    fun auditLog() {
        println("Logging transaction...")
    }

    fun calculateInterest() {
        println("Calculating interest...")
    }

    fun deposit(amount: Double) {
        println("Depositing $$amount")
    }
}

Task: Rewrite the BankVault class applying private, internal, and protected modifiers to meet the requirements above. (Note: You will need to make the class open to allow for the protected modifier to make sense for future subclasses).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.