Kotlin adds an `internal` visibility modifier that Java doesn't have as a keyword. What is `internal` scoped to, and how does that differ from Java's package-private default access?
answer
- internal = module-scoped, not package-scoped
- module = one Gradle/Maven compile unit
- name-mangled in bytecode, not JVM-enforced
- coarser than Java package-private
- doesn't protect between packages in same module
basics
~20 sIn Kotlin, internal means 'visible anywhere inside this compiled module (like one Gradle project or JAR), but hidden outside it.' Java's package-private is scoped to a single package folder instead, which is a much smaller boundary.
solid answer
~50 sKotlin's internal is scoped to the compilation module — everything compiled together in one Gradle/Maven module — not to a package. Classes in com.app.order and com.app.payment inside the same Gradle module can freely see each other's internal declarations, while an external module importing the JAR cannot, even using the exact same package name. This is coarser-grained than Java's package-private (whole module vs. single package) but maps much better onto real deployable units like a Spring Modulith module, a library JAR, or a service's own codebase, where you want a broad 'ours vs theirs' line rather than folder-level hiding. The compiler enforces it structurally, and at the JVM bytecode level internal members get name-mangled (a suffix) so Java callers technically can still call them by their mangled name — it's a Kotlin-language-level guarantee, not JVM-level strong encapsulation like JPMS.
go deeper
Should know internal exists in Kotlin and roughly means 'hidden outside this project/module,' without needing exact mangling details.
Should correctly state internal is module-scoped not package-scoped, and know it's coarser than Java package-private.
Should recognize internal alone doesn't separate sub-boundaries within one module and knows to pair it with architecture tests for finer-grained rules.
Should be able to decide, for a specific system, whether module boundaries should be enforced at the Gradle-module level (internal), the intra-module package level (ArchUnit/Modulith), or both, and justify the choice against team size and deployment topology.
## Kotlin's four visibility modifiers Kotlin defines four visibility modifiers: | Modifier | Scope | |---|---| | `private` | visible only within the containing class/file — top-level private declarations are file-scoped, a difference from Java's `private` | | `protected` | subclasses, no file-scope | | `internal` | visible anywhere in the same compilation module — a Gradle source set, Maven module, or IntelliJ module, compiled together as one unit | | `public` | default if omitted, visible everywhere | `internal` **cuts across package boundaries entirely**: two classes in totally different packages, `com.app.order` and `com.app.payment`, can both see each other's internal declarations as long as they're compiled in the same module. ## How the compiler enforces it At the bytecode level the Kotlin compiler enforces this by **name-mangling** internal members, appending a suffix derived from the module name, so that even if a Java class outside the module tries to call the mangled method by guessing its Kotlin source name, the actual bytecode-level method name doesn't match — a soft technical deterrent, not a hard block, since a sufficiently determined caller can still look up and invoke the mangled name via reflection or by reading the bytecode. ## Why Kotlin added it Kotlin's designers added `internal` because package-private, Java's finest built-in tool short of public, doesn't map onto how most real projects organize code. Modern projects rarely put everything in one flat package; instead they split code into many packages that are conceptually one deployable/reusable unit: - a library JAR - a Spring Modulith module - a service `internal` gives exactly that "ours vs theirs" boundary: anything inside the module is trusted to collaborate freely across packages, while nothing outside the module — a separate JAR, another team's module, an external consumer of your library — can see it, regardless of how many internal packages the implementation is split across. ## The trade-off — coarseness cuts both ways `internal` is coarser-grained than package-private in the direction that matters for most real designs, module-wide rather than package-wide, but that coarseness cuts both ways. Within a large single-module monolith, `internal` gives you essentially no protection between sub-teams' packages — everything internal to the module is visible to everyone else working in that same module, so two feature teams sharing one Gradle module can still freely reach into each other's "internal" classes, because the module boundary, not the package boundary, is what Kotlin actually checks. This is precisely the gap Spring Modulith and ArchUnit exist to close within a single module: they add package-level rules — allowed dependencies between named application modules — on top of Kotlin's module-level `internal`, giving you both a coarse module-wide seal and a finer intra-module map of who's allowed to depend on whom. ## Failure modes 1. **A common mistake** is assuming `internal` gives Java-style package isolation between packages inside the same module — it doesn't, so a team restructuring code into separate packages purely for internal organization gets no additional protection from doing so; only extracting genuinely separate Gradle modules does. 2. **Another failure mode:** because internal mangling is name-based obfuscation rather than JVM-level access control (unlike JPMS `exports`), it is not a security boundary — reflection, or plain Java code compiled against the mangled bytecode name, can still reach in; teams sometimes over-trust `internal` as "impossible to access from outside" when it's really "inconvenient, not impossible." 3. **A third:** mixed Kotlin/Java codebases can be surprised that Java code in the same module sees Kotlin `internal` members as plain public (with the mangled name), since Java has no concept of `internal` at all — the compiler enforces the rule only when the caller is itself Kotlin source being compiled by the same invocation. ## Where it shows up — a Spring Modulith backend In a Spring Modulith Kotlin backend, a typical module (say `catalog`) exposes a single public `CatalogApi` interface and data transfer objects, while its `CatalogService` implementation, JPA entities, and repository interfaces sit in an internal package and are declared `internal class`. Other Spring Modulith modules in the same Gradle module — the whole backend compiled as one build — can only call `CatalogApi`; the compiler will not let a `sync` module reference `CatalogService` directly, because `internal` combined with Kotlin's compiler-enforced module scoping blocks it at compile time, the same way package-private blocks cross-package access in Java, but scoped to the far more meaningful "one deployable module" unit rather than an arbitrary folder.
- If two feature teams both work inside the same Gradle module, does marking a class internal protect one team's implementation from the other team's code?No — internal visibility is scoped to the whole compilation module, so any code compiled as part of that module, regardless of team or package, can see all internal declarations. Protecting sub-boundaries within one module requires an additional mechanism like an ArchUnit or Spring Modulith rule that checks allowed package-to-package dependencies, since Kotlin's own visibility system stops at the module line.
- Since internal members are only name-mangled rather than truly hidden at the JVM level, can a Java class outside the module ever call a Kotlin internal method?Yes, technically — the method compiles as a public method in bytecode with a mangled name, so Java code that knows or discovers that mangled name can invoke it directly or via reflection. It's a compile-time deterrent enforced by the Kotlin compiler for Kotlin callers, not a JVM access-control boundary like JPMS's exports, so it shouldn't be relied on as a security guarantee.
- How does internal interact with Spring's component scanning — can Spring instantiate and wire an internal class as a bean from another module?Yes, Spring uses reflection to construct and wire beans, so an internal class annotated @Service or @Component is instantiated and injected without issue even though ordinary Kotlin code outside the module can't reference the class by name at compile time. internal is a compile-time authoring boundary for developers, not a runtime restriction on the framework.
internal is like a badge that lets you into any room in your company's building, no matter which floor or department — but it won't get you into a different company's building next door, even if the room numbers match.
saying these in an interview costs you the question
- Confuses Kotlin internal with Java package-private and claims it's package-scoped
- Believes internal is a hard security boundary immune to reflection
- Doesn't know internal visibility is scoped to the Gradle/Maven compilation module
- Assumes splitting internal code into more packages inside one module adds protection
- Can't explain why Spring can still instantiate an internal class as a bean