# Kotlin Internal vs Private: Visibility Modifiers Explained with Code Examples

> Explore kotlin internal vs private visibility. Learn when to use private for file/class access and internal for module-wide exposure while keeping code private to your module.

- Repository: [JetBrains/kotlin](https://github.com/jetbrains/kotlin)
- Tags: deep-dive
- Published: 2026-02-19

---

**Use `private` to restrict access to a single file or class, and `internal` to expose declarations to an entire Kotlin module while hiding them from external consumers.**

When architecting Kotlin applications in the JetBrains/kotlin ecosystem, choosing between `internal` and `private` visibility modifiers directly impacts your API surface and encapsulation boundaries. Understanding how the compiler enforces these rules—down to the JVM bytecode level—helps you write maintainable libraries and applications that leverage kotlin internal vs private scoping effectively.

## Understanding Visibility Scope in Kotlin

Kotlin’s visibility modifiers create distinct boundaries that determine where a declaration can be accessed. The compiler implements these checks in [`core/compiler.common/src/org/jetbrains/kotlin/descriptors/Visibilities.kt`](https://github.com/JetBrains/kotlin/blob/main/core/compiler.common/src/org/jetbrains/kotlin/descriptors/Visibilities.kt), where each modifier is defined as a concrete object extending the base `Visibility` class.

### Private Visibility: File and Class Boundaries

The `private` modifier enforces the strictest encapsulation. For **top-level declarations** (functions, properties, or classes declared directly in a file), `private` restricts access to that specific file only. For **members** of a class or interface, `private` restricts access to the body of that class, including its companion objects.

According to the Kotlin compiler source, `object Private` in [`Visibilities.kt`](https://github.com/JetBrains/kotlin/blob/main/Visibilities.kt) implements `Visibility("private", isPublicAPI = false)` with `mustCheckInImports()` returning `true`, forcing the compiler to verify visibility across import statements.

### Internal Visibility: Module-Level Access

The `internal` modifier exposes declarations to the entire **Kotlin module**—defined as a set of Kotlin files compiled together, such as a Gradle `:lib` project or a Maven module. While `internal` members are accessible from any file within the same module, they remain invisible to other modules that depend on it.

In [`Visibilities.kt`](https://github.com/JetBrains/kotlin/blob/main/Visibilities.kt), `object Internal` implements `Visibility("internal", isPublicAPI = false)` and also requires import checking. This modifier strikes a balance between encapsulation and internal reusability, allowing you to expose implementation helpers to the rest of your module without polluting your public API.

## Compiler Implementation: How Kotlin Enforces Visibility

The Kotlin compiler enforces visibility through a combination of descriptor objects, comparison logic, and JVM bytecode generation strategies.

### Visibility Objects in Visibilities.kt

The concrete implementations in [`core/compiler.common/src/org/jetbrains/kotlin/descriptors/Visibilities.kt`](https://github.com/JetBrains/kotlin/blob/main/core/compiler.common/src/org/jetbrains/kotlin/descriptors/Visibilities.kt) define the behavior of each modifier:

```kotlin
// core/compiler.common/src/org/jetbrains/kotlin/descriptors/Visibilities.kt
object Private : Visibility("private", isPublicAPI = false) {
    override fun mustCheckInImports(): Boolean = true
}

object Internal : Visibility("internal", isPublicAPI = false) {
    override fun mustCheckInImports(): Boolean = true
}

```

Both modifiers force the compiler to check imports, but they differ in their scope resolution logic elsewhere in the compiler pipeline.

### Visibility Comparison and Ordering

The compiler ranks visibility strength using `ORDERED_VISIBILITIES` in [`Visibilities.kt`](https://github.com/JetBrains/kotlin/blob/main/Visibilities.kt), which assigns numeric values to each modifier:

- `Private` and `PrivateToThis`: **0** (most restrictive)
- `Internal` and `Protected`: **1** (module or inheritance scope)
- `Public`: **2** (least restrictive)

This ordering determines visibility conflicts during overload resolution and inheritance checking. When comparing kotlin internal vs private, `private` is strictly more restrictive than `internal`.

### JVM Bytecode Generation

At the bytecode level, the modifiers translate differently:

- **`private` members** compile to **private** JVM members (or package-private for top-level private declarations), enforcing restrictions at the JVM level.
- **`internal` members** compile to **public** JVM members but receive a synthetic module-name suffix (e.g., `myFunction$module_name`). The Kotlin compiler emits metadata that causes other modules to reject these calls at compile time, while the JVM sees them as public.

This implementation detail explains why `internal` APIs can appear public in Java interop scenarios unless marked with `@PublishedApi` or similar mechanisms.

## Practical Code Examples

Consider a multi-module Gradle project with a `:lib` module and an `:app` module.

**File: lib/src/main/kotlin/com/example/utils.kt**

```kotlin
internal class Helper {               // visible throughout the `lib` module
    fun assist() = "assist"
}

private fun secret() = "shh"          // only this file can call it

```

**File: lib/src/main/kotlin/com/example/api.kt**

```kotlin
fun useHelper() {
    // OK: same module, internal class is accessible
    val h = Helper()
    println(h.assist())
    
    // println(secret()) // compile error: `secret` is private to utils.kt
}

```

**File: app/build.gradle.kts** (depends on `:lib`)

```kotlin
dependencies {
    implementation(project(":lib"))
}

```

**File: app/src/main/kotlin/com/example/App.kt**

```kotlin
fun main() {
    // ERROR – `Helper` is internal to the `lib` module, not visible here
    // val h = Helper() // compile-time error
    
    // `secret()` is also inaccessible (and invisible) from this module
}

```

## Summary

- **`private`** restricts access to the containing file (top-level) or class (members), providing the strongest encapsulation within the Kotlin compiler's visibility hierarchy (rank 0).
- **`internal`** exposes declarations to the entire Kotlin module while hiding them from external modules, striking a balance between reusability and API hygiene (rank 1).
- The compiler enforces these rules through [`Visibilities.kt`](https://github.com/JetBrains/kotlin/blob/main/Visibilities.kt) and [`Visibility.kt`](https://github.com/JetBrains/kotlin/blob/main/Visibility.kt), generating JVM bytecode that makes `private` members strictly private while using synthetic naming and metadata to enforce `internal` boundaries.
- Choose `private` for implementation details that should never leak, and `internal` for module-wide utilities that support your public API without exposing them to consumers.

## Frequently Asked Questions

### Can Java code access Kotlin internal members?

No. While `internal` members compile to public JVM bytecode with synthetic module-name suffixes, the Kotlin compiler emits metadata that prevents other modules from accessing them. Java code in a different module will not see these members as accessible, though they may appear visible in the bytecode. Java code within the same module can access `internal` members because the compiler treats them as part of the same compilation unit.

### Why does the Kotlin compiler rank private as more restrictive than internal?

The compiler uses an ordered visibility map in [`Visibilities.kt`](https://github.com/JetBrains/kotlin/blob/main/Visibilities.kt) where `private` receives rank 0 and `internal` receives rank 1. This ranking reflects the scope size: `private` limits access to a single file or class, while `internal` expands access to the entire module. During overload resolution and visibility checking, the compiler uses these ranks to determine if a candidate declaration is visible from a given context, ensuring that `private` always takes precedence as the most restrictive modifier.

### When should I use internal instead of private for top-level declarations?

Use `internal` when you need to share implementation details across multiple files within the same module, such as utility functions, extension functions, or helper classes that support your public API but should not be exposed to module consumers. Use `private` for top-level declarations that are strictly local to a single file, such as file-private constants or helper functions that only one class or function in that file requires. If other files in the module need the declaration, `internal` is the correct choice; if not, prefer `private` for stronger encapsulation.

### How does internal visibility affect binary compatibility?

Changing `internal` members requires recompilation of dependent modules because the compiler embeds module-specific metadata and synthetic naming (such as the `$module_name` suffix) into the bytecode. While `internal` members are not part of the public API contract, modifications to them can break compilation for other modules within your project that reference these members. In contrast, `private` members can be changed freely without affecting any other compilation units, as they are completely invisible outside their containing file or class. Treat `internal` APIs as stable within your module ecosystem to avoid unnecessary recompilation cascades.