Explain the source-set hierarchy in a Kotlin Multiplatform module and how an intermediate source set like `iosMain` fits in.
answer
- dependsOn edges form a tree
- commonMain is the root, compiled for all
- intermediate sets: iosMain, nativeMain, appleMain
- applyDefaultHierarchyTemplate() auto-wires them
- code must compile for all consuming targets
basics
~20 sSource sets form a tree: commonMain is the root, and each platform source set dependsOn it. Intermediate sets like iosMain group several related native targets (iosArm64, iosSimulatorArm64) so they can share code above common but below the individual targets.
solid answer
~40 sKMP organizes code as a **source-set hierarchy**. `commonMain` sits at the top and is compiled for every declared target. Each leaf target (e.g. `iosArm64`, `iosSimulatorArm64`) has its own source set that ultimately `dependsOn` `commonMain`. Between them you can have **intermediate source sets** — `iosMain` is the classic example, grouping the Apple targets so iOS-specific-but-target-agnostic code lives in one place instead of being duplicated per architecture. The default hierarchy template (enabled by `applyDefaultHierarchyTemplate()`, on by default in recent KGP) creates these intermediate sets automatically: `nativeMain`, `appleMain`, `iosMain`, etc. The rule is that a child source set sees the declarations of its parents, and code in any set may only use APIs available to *all* targets it ultimately compiles for. This is what makes `expect`/`actual` resolution well-defined.
code
kotlin · 19 lineskotlin {
// default hierarchy template auto-creates nativeMain/appleMain/iosMain
iosArm64()
iosSimulatorArm64()
jvm()
sourceSets {
commonMain.dependencies {
implementation("io.ktor:ktor-client-core:2.3.0")
}
// shared by both iOS architectures, hidden from jvm
iosMain.dependencies {
implementation("io.ktor:ktor-client-darwin:2.3.0")
}
jvmMain.dependencies {
implementation("io.ktor:ktor-client-okhttp:2.3.0")
}
}
}go deeper
Knowing that commonMain is shared and jvmMain/iosMain are platform-specific is enough.
Explain the dependsOn tree, intermediate source sets, and the default hierarchy template; get the dependency direction right.
Discuss the compile-for-all-targets visibility rule, custom hierarchies, and how the hierarchy underpins expect/actual resolution.
Weigh hierarchy design across many targets, the maintenance/CI cost of custom hierarchies vs the default template, and code-sharing strategy at scale.
## The hierarchy concept In KMP, **source sets** are not a flat list — they form a directed graph (effectively a tree) connected by `dependsOn` edges. The semantics of `a.dependsOn(b)` is: source set `a` can *see and override* declarations from `b`, and `a` is compiled for a subset of the targets `b` is compiled for. The root is `commonMain`, which is compiled for **every** declared target. Each platform target gets a leaf source set (e.g. `jvmMain`, `iosArm64Main`) that transitively `dependsOn(commonMain)`. ## Intermediate source sets Real projects rarely want code in only `commonMain` or only one leaf. Suppose you declare `iosArm64()` and `iosSimulatorArm64()` and want iOS code shared between them but *not* visible to the JVM. That shared layer is an **intermediate source set**: `iosMain`. It `dependsOn(commonMain)`, and both `iosArm64Main` and `iosSimulatorArm64Main` `dependsOn(iosMain)`: ``` commonMain └── nativeMain └── appleMain └── iosMain ├── iosArm64Main └── iosSimulatorArm64Main ``` Code in `iosMain` may use Apple/native APIs (because every target below it is an Apple target) but is shared across the iOS architectures. ## The default hierarchy template Hand-wiring `dependsOn` is error-prone, so KGP ships `applyDefaultHierarchyTemplate()`. In modern Kotlin (1.9.20+) it is applied automatically when you don't define a custom hierarchy. It creates standard intermediate sets — `nativeMain`, `appleMain`, `iosMain`, `tvosMain`, `linuxMain`, etc. — based on which targets you declared. You then just write into `iosMain` and it exists. You access them in the DSL: ```kotlin kotlin { iosArm64() iosSimulatorArm64() jvm() sourceSets { iosMain.dependencies { implementation("io.ktor:ktor-client-darwin:2.3.0") } } } ``` ## The compile-for-all rule The governing constraint: code in a source set must compile for **all** targets that ultimately consume it. `commonMain` therefore can't reference `java.io.File`; `iosMain` can use Foundation but not the JVM. This rule is what lets the compiler resolve `expect` declarations in common against `actual` declarations in the appropriate platform/intermediate set — though the `expect`/`actual` *language* mechanism itself belongs to Kotlin proper, not the Gradle plugin. ## Why interviewers ask this The hierarchy is the part of KMP that is purely about the *build* model — how Gradle compilations are layered and how shared code is scoped. Getting the `dependsOn` direction right and knowing intermediate sets exist (and are usually auto-generated) is the expected middle-level competency.
- What does `applyDefaultHierarchyTemplate()` do, and do you usually need to call it explicitly?It creates the standard intermediate source sets (`nativeMain`, `appleMain`, `iosMain`, etc.) and wires their `dependsOn` edges based on your declared targets. In Kotlin 1.9.20+ it is applied automatically unless you define a custom hierarchy, so you rarely call it by hand.
- Which direction does `dependsOn` point — from common to platform or platform to common?From platform to common: `iosMain.dependsOn(commonMain)`. The child (more specific, fewer targets) depends on the parent (more general). It is not a regular Gradle `dependencies { }` edge — it is a KMP source-set relationship that controls visibility and `expect`/`actual` resolution.
saying these in an interview costs you the question
- Reversing `dependsOn` (saying common depends on platform).
- Claiming you must hand-create every intermediate source set — the default template generates them.
- Saying `iosMain` can use JVM APIs.