skip to content

Explain the KMP source-set hierarchy and how intermediate source sets like appleMain enable code sharing.

level: middleimportance: should knowfreq 50%

answer

  1. Source sets form a dependsOn tree
  2. commonMain root, leaf sets per target
  3. Intermediate sets (appleMain, nativeMain) share grouped code
  4. Visibility = intersection of descendants + ancestors
  5. Default hierarchy template wires it automatically

basics

~10 s

Source sets form a tree. commonMain is the root every target uses. Intermediate sets like appleMain group related targets (iOS, macOS) so you can share code among them without duplicating it per target.

solid answer

~40 s

A KMP project is a hierarchy of **source sets**. `commonMain` is the root, shared by all targets. Each target has a leaf set (`androidMain`, `iosArm64Main`, `jsMain`). Between them sit **intermediate source sets** like `appleMain`, `nativeMain`, or `iosMain` that group platforms sharing capabilities. Code in an intermediate set sees APIs common to all of its descendants — e.g. `appleMain` can use Foundation/Darwin APIs shared by iOS+macOS. The modern toolchain provides a **default hierarchy template** (`applyDefaultHierarchyTemplate()`, often automatic) that wires these sets for you. A source set 'depends on' its parent and inherits its visible API surface; a child can `actual`-ize an `expect` declared in any ancestor. This avoids copy-pasting platform code across many native targets.

go deeper

for a junior

Knows commonMain is shared and each platform has its own source set.

for a middle

Describes intermediate source sets, the dependsOn tree, and how visibility flows.

for a senior

Uses the default hierarchy template, actualizes expects at intermediate level, and reasons about API intersection visibility.

for a principal

Designs source-set topology for many targets, minimizing duplication and controlling which platform APIs leak into which layer.

## Source sets as a dependency tree A **source set** is a named group of Kotlin files with its own dependencies and visible API surface. KMP arranges them into a **hierarchy (a tree of `dependsOn` edges)**: ``` commonMain ├── jvmMain (Android/JVM) ├── jsMain └── nativeMain └── appleMain ├── iosMain │ ├── iosArm64Main │ └── iosSimulatorArm64Main └── macosMain ``` - **`commonMain`** — the root; visible to every target; can use only multiplatform APIs. - **Leaf sets** — e.g. `iosArm64Main`, one per concrete target. - **Intermediate source sets** — e.g. `nativeMain`, `appleMain`, `iosMain` — group targets that share capabilities. ## Why intermediate sets matter Without them, code that works on all Apple targets would have to be copy-pasted into `iosArm64Main`, `iosSimulatorArm64Main`, `macosArm64Main`, etc. An intermediate set like **`appleMain`** is compiled once and shared by all Apple descendants, and it can see the **Foundation/Darwin** APIs common to them. Similarly `nativeMain` can use POSIX-style APIs common to all native targets. ## API visibility follows the tree A source set sees: - the **intersection** of APIs available to all of its descendant targets, plus - everything declared in its **ancestor** source sets. So `appleMain` sees `commonMain` + `nativeMain` + the Apple-common platform APIs, but **not** Android or JS APIs. ## expect/actual across the hierarchy An `expect` declared in `commonMain` can be `actual`-ized in an **intermediate** set (e.g. one `actual` in `appleMain` covering all Apple targets) instead of repeating it in each leaf — a big duplication win. ## The default hierarchy template Modern Kotlin auto-applies a **default hierarchy template** (you can call `applyDefaultHierarchyTemplate()` explicitly) that creates `nativeMain`, `appleMain`, `iosMain`, etc. based on the targets you declare. Before this, teams hand-wired `dependsOn` edges. Each `*Main` set has a parallel `*Test` set (`commonTest`, `appleTest`, …) following the same tree. ```kotlin kotlin { androidTarget() iosArm64(); iosSimulatorArm64() // applyDefaultHierarchyTemplate() // usually implicit sourceSets { commonMain.dependencies { /* shared libs */ } // appleMain is created for you and groups iOS targets } } ```

  • Why can appleMain call Foundation APIs but commonMain cannot?
    appleMain's descendants are all Apple targets, so the Foundation API is common to them; commonMain must support every target, so it only sees fully multiplatform APIs.
  • Can you actualize an expect once for all Apple targets?
    Yes — provide the actual in the appleMain intermediate set instead of repeating it in each leaf source set.

Like a family tree: shared traits at the top, branch-specific traits at intermediate nodes, individual quirks at the leaves.

saying these in an interview costs you the question

  • Thinking every target needs its own copy of shared native code
  • Believing commonMain can see Foundation/Darwin APIs
  • Not knowing intermediate source sets exist
  • Confusing source sets with Gradle modules
  • Forgetting test source sets mirror the same tree

context