ProgrammingBackend Developer

How do visibility modifiers (internal/private/protected/public) work for top-level functions and properties in Kotlin? What are the differences with Java and what nuances should be considered?

Pass interviews with Hintsage AI assistant

Answer.

In Kotlin, visibility modifiers allow control over access to declarations: classes, properties, functions, and top-level (file-level) entities. Unlike Java, where modifiers apply only at the class level, in Kotlin they work for top-level declarations as well, which is important for structuring large projects and library APIs.

Background

In Java, there are no visibility modifiers for functions or properties outside a class—all reside within a public (or package-private) class. Kotlin, however, often structures projects differently, where functions or properties reside not within a class, but directly in a file.

The Issue

Java developers often expect that public by default works the same way as in Java, but in Kotlin, a top-level function (or property) is visible in all modules unless otherwise marked. Incorrect visibility can lead to lexical cluttering of the public API, unexpected availability of internal utilities, or unavailability of necessary public functions.

The Solution

Kotlin provides the following modifiers:

  • public: the declaration is visible everywhere (is the default modifier for top-level).
  • internal: the declaration is visible within all files of the same module (one gradle module, one compiled artifact, one jar).
  • private: visible only within the same file/class where declared. For top-level - only within the file.
  • protected: not applicable for top-level declarations, only for classes/interfaces and their subclasses.

Example:

// file: Foo.kt private fun utilityFun() {} internal val bar: Int = 10 public val baz: Int = 20 // public is optional fun printValue() { println(bar) }

Key Features:

  • internal restricts visibility by module (jar/artifact), not by package.
  • protected cannot be used for top-level functions or properties.
  • private at top-level limits the declaration to the current file.

Trick Questions.

Can protected be used for a top-level function?

No, protected is only relevant for members of a class/interface; top-level elements do not support this.

If a top-level function is declared internal, will it be visible in other modules?

No. It will only be visible within the current jar/Gradle module.

What is the difference between a private class and a private top-level function?

  • private class: visible only within the current file, cannot be used outside the file.
  • private top-level function or property: similarly visible only within the file.

Example:

// file: Utils.kt private fun helper() { /* ... */ } // visible only in this file internal fun useful() { /* ... */ } // visible throughout the module

Common Mistakes and Anti-Patterns

  • Using public by default for all declarations leads to "clutter" in auto-complete and APIs.
  • Using internal for libraries intended for external clients hides necessary public APIs.
  • Confusion with protected and attempts to apply them at top-level.

Real-Life Example

Negative Case

Test utilities declared public end up in the artifact, causing issues for the library client—all that is not related to the public API becomes visible.

Pros:

  • Quick integration.

Cons:

  • The size of the public API grows, "accidental" methods become accessible.

Positive Case

Internal functions are declared private, utilities with internal visibility for common use within the module, only carefully considered interfaces have public access.

Pros:

  • Clear, clean API structure.
  • Random dependencies are minimized.

Cons:

  • The need to carefully think about the project structure.