How do I use visibility modifiers (private, internal, protected, public) in Kotlin classes?

In Kotlin, visibility modifiers control where classes, properties, functions, and constructors can be accessed from.

The main visibility modifiers are:

  • public
  • private
  • protected
  • internal

If you do not specify a visibility modifier, Kotlin uses public by default.


1. public

public means the declaration can be accessed from anywhere.

class User {
    public val name: String = "Alice"

    public fun sayHello() {
        println("Hello, $name")
    }
}

Because public is the default, this is equivalent:

class User {
    val name: String = "Alice"

    fun sayHello() {
        println("Hello, $name")
    }
}

Use public when you want something to be part of the normal external API of a class.


2. private

Inside a class, private means the member can only be accessed inside that same class.

class BankAccount {
    private var balance: Double = 0.0

    fun deposit(amount: Double) {
        if (amount > 0) {
            balance += amount
        }
    }

    fun getBalance(): Double {
        return balance
    }
}

Usage:

fun main() {
    val account = BankAccount()

    account.deposit(100.0)

    println(account.getBalance())

    // Error:
    // println(account.balance)
}

Here, balance is hidden from outside code. The class controls access through deposit() and getBalance().

Private class members

class UserService {
    private fun validateName(name: String): Boolean {
        return name.isNotBlank()
    }

    fun createUser(name: String) {
        if (validateName(name)) {
            println("User created: $name")
        }
    }
}

validateName() is an implementation detail and cannot be called from outside UserService.


3. protected

protected means the member is visible inside:

  1. The class where it is declared
  2. Its subclasses

It is not visible to general outside code.

open class Animal {
    protected val name: String = "Animal"

    protected fun makeSound() {
        println("Some sound")
    }
}

class Dog : Animal() {
    fun bark() {
        println(name)
        makeSound()
        println("Woof!")
    }
}

Usage:

fun main() {
    val dog = Dog()

    dog.bark()

    // Error:
    // println(dog.name)

    // Error:
    // dog.makeSound()
}

Dog can access name and makeSound() because it inherits from Animal, but outside code cannot.

Important note about protected

In Kotlin, protected is mainly for class inheritance. Unlike Java, Kotlin does not make protected members visible to the whole package.


4. internal

internal means the declaration is visible within the same module.

A module is usually something like:

  • a Gradle source set
  • a Maven project
  • an IntelliJ IDEA module
  • a set of files compiled together
internal class InternalLogger {
    fun log(message: String) {
        println("LOG: $message")
    }
}

Code in the same module can use it:

fun main() {
    val logger = InternalLogger()
    logger.log("Application started")
}

But code from another module cannot access InternalLogger.

Use internal when you want something available across your project/module but not exposed as a public API to other modules.


Visibility in class members

You can apply visibility modifiers to properties and functions:

class Profile {
    public val username: String = "guest"
    private val passwordHash: String = "abc123"
    internal val accountId: Int = 42
    protected val role: String = "user"

    public fun showUsername() {
        println(username)
    }

    private fun checkPassword() {
        println(passwordHash)
    }
}

However, protected only makes sense in classes that may be inherited from:

open class BaseController {
    protected fun authorize() {
        println("Checking permissions")
    }
}

class UserController : BaseController() {
    fun getUser() {
        authorize()
        println("Returning user")
    }
}

Visibility for constructors

You can also control constructor visibility.

Private constructor

Useful when you want to restrict object creation.

class DatabaseConnection private constructor() {
    companion object {
        fun create(): DatabaseConnection {
            return DatabaseConnection()
        }
    }
}

Usage:

fun main() {
    val connection = DatabaseConnection.create()

    // Error:
    // val direct = DatabaseConnection()
}

Visibility for property setters

A common Kotlin pattern is to expose a property for reading but restrict writing.

class Counter {
    var count: Int = 0
        private set

    fun increment() {
        count++
    }
}

Usage:

fun main() {
    val counter = Counter()

    counter.increment()
    println(counter.count)

    // Error:
    // counter.count = 10
}

Here:

  • count can be read from outside the class
  • count can only be changed inside the class

You can also use protected set or internal set:

open class Document {
    var status: String = "draft"
        protected set
}

Subclasses can change status, but outside code cannot.


Top-level visibility

Kotlin also allows functions, properties, and classes outside classes.

private fun helper() {
    println("Only visible in this file")
}

internal fun moduleHelper() {
    println("Visible in the same module")
}

public fun publicApi() {
    println("Visible everywhere")
}

For top-level declarations:

Modifier Meaning
public Visible everywhere
private Visible only in the same file
internal Visible in the same module
protected Not allowed at top level

Quick comparison

Modifier Class member visibility
public Visible everywhere
private Visible only inside the declaring class
protected Visible inside the declaring class and subclasses
internal Visible inside the same module

For top-level declarations:

Modifier Top-level visibility
public Visible everywhere
private Visible only in the same file
internal Visible inside the same module
protected Not allowed

Practical example

open class Account(
    public val owner: String,
    private var balance: Double
) {
    internal val accountNumber: String = "ACC-123"

    protected fun canWithdraw(amount: Double): Boolean {
        return amount <= balance
    }

    fun deposit(amount: Double) {
        if (amount > 0) {
            balance += amount
        }
    }

    fun currentBalance(): Double {
        return balance
    }
}

class SavingsAccount(owner: String, balance: Double) : Account(owner, balance) {
    fun withdraw(amount: Double) {
        if (canWithdraw(amount)) {
            println("Withdrawal allowed")
        } else {
            println("Insufficient funds")
        }
    }
}

Usage:

fun main() {
    val account = SavingsAccount("Alice", 500.0)

    println(account.owner)
    println(account.currentBalance())

    account.deposit(100.0)
    account.withdraw(50.0)

    // Error: private
    // println(account.balance)

    // Error: protected
    // account.canWithdraw(100.0)

    // Accessible only in the same module:
    // println(account.accountNumber)
}

Rule of thumb

Use the most restrictive visibility that still works:

  • Use private for implementation details.
  • Use protected for behavior intended only for subclasses.
  • Use internal for code shared inside a module but hidden from external modules.
  • Use public for APIs that other code is expected to use.

In most Kotlin classes, you will commonly use private for internal state and leave only the necessary functions or properties public.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.