Back to Architecture & Design Patterns

Kotlin Multiplatform Architectural Guidance

kotlinkmparchitecturegradlemultiplatform
⭐ 1.5kπŸ“„ MITπŸ•’ 2026-06-16Source β†—

Install this skill

npx skills add vitorpamplona/amethyst

Works across Claude Code, Cursor, Codex, Copilot & Antigravity

This skill provides a structured decision-making framework for Kotlin Multiplatform (KMP) development, specifically for projects sharing code between JVM environments and mobile platforms. It helps developers determine the optimal placement of logic within source sets, balancing the reuse of code with the need for platform-specific APIs. By applying a clear decision tree, developers can choose between common code, expect/actual declarations, or shared JVM layers like jvmAndroid. This approach prevents unnecessary abstraction for volatile UI or platform-specific paradigms while ensuring core business logic remains unified across Android, iOS, and desktop targets. It is essential for teams managing complex KMP projects who need to maintain clean dependencies and avoid over-engineering their multiplatform codebase.
By vitorpamplona

What this skill does

  • β€’Navigate multiplatform source set placement decisions
  • β€’Identify when to use commonMain versus platform-specific modules
  • β€’Implement the jvmAndroid pattern for shared JVM library overhead
  • β€’Evaluate maintainability costs versus code duplication
  • β€’Define clear boundaries for platform-specific crypto and networking

When to use it

  • βœ“Refactoring shared business logic into common modules
  • βœ“Determining if a library should be scoped to jvmAndroid or commonMain
  • βœ“Resolving complex dependency graph issues between Android and iOS
  • βœ“Assessing whether to implement expect/actual for hardware-specific APIs

When not to use it

  • βœ•Projects targeting only a single platform
  • βœ•Refactoring UI code that relies heavily on platform-native view hierarchies
  • βœ•Situations where simple code duplication is cheaper than creating an abstraction

How to invoke it

Example prompts that trigger this skill:

  • β€œDecide whether to place this new module in commonMain or androidMain.”
  • β€œExplain the pros and cons of using expect/actual for this crypto implementation.”
  • β€œHelp me refactor these shared JVM dependencies into a jvmAndroid source set.”
  • β€œShow me how to structure my Kotlin source sets for better reusability.”
  • β€œDetermine if my navigation logic should be shared or kept platform-specific.”

Example workflow

  1. Analyze the platform dependency of the target code.
  2. Check the decision tree: Does this code run on more than two platforms?
  3. Determine if platform APIs are involved or if it is pure Kotlin.
  4. If JVM-exclusive, apply the jvmAndroid source set pattern.
  5. Define expect/actual interfaces for platform-variable security or I/O.
  6. Verify the final dependency graph to ensure clean separation.

Prerequisites

  • –Kotlin Multiplatform plugin configured
  • –Understanding of Gradle build scripts (KTS)
  • –Existing multi-target project structure

Pitfalls & limitations

  • !Over-abstracting code that is inherently platform-specific
  • !Polluting commonMain with code that requires platform-specific libraries
  • !Neglecting the overhead of managing expect/actual pairs

FAQ

What is the primary difference between commonMain and jvmAndroid?
commonMain is intended for pure Kotlin code shared across all targets, whereas jvmAndroid is a specific pattern for sharing JVM-exclusive libraries between Android and Desktop.
When should I prefer code duplication over abstraction?
You should choose duplication when the maintenance cost of an abstraction, such as complex expect/actual boilerplate, outweighs the simplicity of having two distinct, readable platform-specific implementations.
Can I use jvmAndroid for web or iOS targets?
No, the jvmAndroid pattern is strictly for JVM-based platforms. Code intended for web or iOS must be moved to commonMain and limited to pure Kotlin or multiplatform-supported libraries.

How it compares

Unlike generic KMP documentation, this skill provides a specific, codebase-proven decision tree that prioritizes architectural pragmatism over theoretical abstraction.

Source & trust

⭐ 1.5k starsπŸ“„ MITπŸ•’ Updated 2026-06-16πŸ›‘ runs-shell

From the source: β€œ# Kotlin Multiplatform: Platform Abstraction Decisions Expert guidance for KMP architecture in Amethyst - deciding what to share vs keep platform-specific. ## When to Use This Skill Making platform abstraction decisions: - "Should I create expect/actual or keep Android-only?" - "Can I share this Vie…”

View the full SKILL.md source

# Kotlin Multiplatform: Platform Abstraction Decisions

Expert guidance for KMP architecture in Amethyst - deciding what to share vs keep platform-specific.

## When to Use This Skill

Making platform abstraction decisions:
- "Should I create expect/actual or keep Android-only?"
- "Can I share this ViewModel logic?"
- "Where does this crypto/JSON/network implementation belong?"
- "This uses Android Context - can it be abstracted?"
- "Is this code in the wrong module?"
- Preparing for iOS/web/wasm targets
- Detecting incorrect placements

## Abstraction Decision Tree

**Central question:** "Should this code be reused across platforms?"

Follow this decision path (< 1 minute):

```
Q: Is it used by 2+ platforms?
β”œβ”€ NO  β†’ Keep platform-specific
β”‚         Example: Android-only permission handling
β”‚
└─ YES β†’ Continue ↓

Q: Is it pure Kotlin (no platform APIs)?
β”œβ”€ YES β†’ commonMain
β”‚         Example: Nostr event parsing, business rules
β”‚
└─ NO  β†’ Continue ↓

Q: Does it vary by platform or by JVM vs non-JVM?
β”œβ”€ By platform (Android β‰  iOS β‰  Desktop)
β”‚  β†’ expect/actual
β”‚  Example: Secp256k1Instance (uses different security APIs)
β”‚
β”œβ”€ By JVM (Android = Desktop β‰  iOS/web)
β”‚  β†’ jvmAndroid
β”‚  Example: Jackson JSON parsing (JVM library)
β”‚
└─ Complex/UI-related
   β†’ Keep platform-specific
   Example: Navigation (Activity vs Window too different)

Final check:
Q: Maintenance cost of abstraction < duplication cost?
β”œβ”€ YES β†’ Proceed with abstraction
└─ NO  β†’ Duplicate (simpler)
```

### Real Examples from Codebase

**Crypto β†’ expect/actual:**
```kotlin
// commonMain - expect declaration
expect object Secp256k1Instance {
    fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray
}

// androidMain - uses Android Keystore
// jvmMain - uses Desktop JVM crypto
// iosMain - uses iOS Security framework
```
**Why:** Each platform has different security APIs.

**JSON parsing β†’ jvmAndroid:**
```kotlin
// quartz/build.gradle.kts
val jvmAndroid = create("jvmAndroid") {
    api(libs.jackson.module.kotlin)
}
```
**Why:** Jackson is JVM-only, works on Android + Desktop, not iOS/web.

**Navigation β†’ platform-specific:**
- Android: `MainActivity` (Activity + Compose Navigation)
- Desktop: `Window` + sidebar + MenuBar
**Why:** UI paradigms fundamentally different.

## Mental Model: Source Sets as Dependency Graph

Think of source sets as a dependency graph, not folders.

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ commonMain = Contract (pure Kotlin)         β”‚
β”‚ - Business logic, protocol, data models     β”‚
β”‚ - No platform APIs                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
             β”‚                      β”‚
             β–Ό                      β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ jvmAndroid        β”‚  β”‚ iosMain          β”‚
   β”‚ JVM libs shared   β”‚  β”‚ iOS common       β”‚
   β”‚ - Jackson         β”‚  β”‚                  β”‚
   β”‚ - OkHttp          β”‚  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜       β”‚
       β”‚           β”‚           β”‚
       β–Ό           β–Ό           β”œβ”€β†’ iosArm64Main
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     └─→ iosSimulatorArm64Main
  β”‚android  β”‚ β”‚jvmMain   β”‚
  β”‚Main     β”‚ β”‚(Desktop) β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Future: jsMain, wasmMain
```

**Key insight:** jvmAndroid is NOT a platform - it's a shared JVM layer.

## The jvmAndroid Pattern

**Unique to Amethyst.** Shares JVM libraries between Android + Desktop.

### When to Use jvmAndroid

Use jvmAndroid when:
- βœ… JVM-specific libraries (Jackson, OkHttp, url-detector)
- βœ… Android implementation = Desktop implementation (same JVM)
- βœ… Library doesn't work on iOS/web

Do NOT use jvmAndroid for:
- ❌ Pure Kotlin code (use commonMain)
- ❌ Platform-specific APIs (use androidMain/jvmMain)
- ❌ Code that should work on all platforms

### Example from quartz/build.gradle.kts

```kotlin
// Must be defined BEFORE androidMain and jvmMain
val jvmAndroid = create("jvmAndroid") {
    dependsOn(commonMain.get())

    dependencies {
        api(libs.jackson.module.kotlin)  // JSON parsing - JVM only
        api(libs.url.detector)            // URL extraction - JVM only
        implementation(libs.okhttp)       // HTTP client - JVM only
    }
}

// Both depend on jvmAndroid
jvmMain { dependsOn(jvmAndroid) }
androidMain { dependsOn(jvmAndroid) }
```

**Why Jackson in jvmAndroid, not commonMain?**
- Jackson is JVM-specific library
- Works on Android (runs on JVM)
- Works on Desktop (runs on JVM)
- Does NOT work on iOS (not JVM) or web (not JVM)

**Web/wasm consideration:** For future web support, consider migrating from Jackson β†’ kotlinx.serialization (see Target-Specific Guidance).

## What to Abstract vs Keep Platform-Specific

Quick decision guidelines based on codebase patterns:

### Always Abstract
- **Crypto** (Secp256k1, encryption, signing)
- **Core protocol logic** (Nostr events, NIPs)
- **Why:** Needed everywhere, platform security APIs vary

### Often Abstract
- **I/O operations** (file reading, caching)
- **Logging** (platform logging systems differ)
- **Serialization** (if using kotlinx.serialization)
- **Why:** Commonly reused, platform implementations available

### Sometimes Abstract
- **Business logic:** YES - state machines, data processing
- **ViewModels:** YES - state + business logic shareable (StateFlow/SharedFlow)
- **Screen layouts:** NO - platform-native (Window vs Activity)
- **Why:** ViewModels contain platform-agnostic state; Screens render differently per platform

### Rarely Abstract
- **Complex UI components** (composables with heavy platform dependencies)
- **Why:** Platform paradigms can differ significantly

### Never Abstract
- **Navigation** (Activity vs Window fundamentally different)
- **Permissions** (Android vs iOS APIs incompatible)
- **Platform UX patterns**
- **Why:** Too platform-specific, abstraction creates leaky APIs

### Evidence from shared-ui-analysis.md

| Component | Shared? | Rationale |
|-----------|---------|-----------|
| PubKeyFormatter, ZapFormatter | βœ… YES | Pure Kotlin, no platform APIs |
| TimeAgoFormatter | ⚠️ ABSTRACTED | Needs StringProvider for localized strings |
| ViewModels (state + logic) | βœ… YES | StateFlow/SharedFlow platform-agnostic, Compose Multiplatform lifecycle compatible |
| Screen layouts (Scaffold, nav) | ❌ NO | Window vs Activity, sidebar vs bottom nav fundamentally different |
| Image loading (Coil) | ⚠️ ABSTRACTED | Coil 3.x supports KMP, needs expect/actual wrapper |

## expect/actual Mechanics

**When to use:** Code needed by 2+ platforms, varies by platform.

### Pattern Categories from Codebase

**Objects (singletons):**
```kotlin
// 24 expect declarations found, common pattern:
expect object Secp256k1Instance { ... }
expect object Log { ... }
expect object LibSodiumInstance { ... }
```

**Classes (instantiable):**
```kotlin
expect class AESCBC { ... }
expect class DigestInstance { ... }
```

**Functions (utilities):**
```kotlin
expect fun platform(): String
expect fun currentTimeSeconds(): Long
```

**See** [references/expect-actual-catalog.md](references/expect-actual-catalog.md) for complete catalog with rationale.

## Target-Specific Guidance

### Android, JVM (Desktop), iOS - Current Primary Targets

**Status:** Mature patterns, stable APIs

**Android (androidMain):**
- Uses Android framework (Activity, Context, etc.)
- secp256k1-kmp-jni-android (`0.23.0` in `libs.versions.toml`) for crypto
- AndroidX libraries

**Desktop JVM (jvmMain):**
- Uses Compose Desktop (Window, MenuBar, etc.)
- secp256k1-kmp-jni-jvm (same `0.23.0` line) for crypto
- Pure JVM libraries

**iOS (iosMain):**
- Mature target β€” actively built and tested
- Architecture targets: iosArm64, iosSimulatorArm64, iosX64 (plus macosArm64 for host tooling)
- Platform APIs via platform.posix, Security framework

### Web, wasm - Future Targets

**Status:** Not yet implemented, consider for future-proofing

**Constraints to know:**
- ❌ No platform.posix (file I/O different)
- ❌ No JVM libraries (Jackson, OkHttp won't work)
- ❌ Different async model (JS event loop vs threads)

**Future-proofing tips:**
1. Prefer pure Kotlin in commonMain
2. Use kotlinx.* libraries:
   - kotlinx.serialization instead of Jackson
   - ktor instead of OkHttp (ktor supports web)
   - kotlinx.datetime instead of custom date handling
3. Avoid platform.posix for file operations
4. Test abstractions work without JVM assumptions

**Example migration path:**
```kotlin
// Current: jvmAndroid (JVM-only)
api(libs.jackson.module.kotlin)

// Future: commonMain (all platforms)
api(libs.kotlinx.serialization.json)
```

## Integration: When to Invoke Other Skills

### Invoke gradle-expert

Trigger gradle-expert skill when encountering:
- Dependency conflicts (e.g., secp256k1-android vs secp256k1-jvm version mismatch)
- Build errors related to source sets
- Version catalog issues (libs.versions.toml)
- "Duplicate class" errors
- Performance/build time issues

**Example trigger:**
```
Error: Duplicate class found: fr.acinq.secp256k1.Secp256k1
```
β†’ Invoke gradle-expert for dependency conflict resolution.

### Flags to Raise

**Platform code in commonMain:**
```kotlin
// ❌ INCORRECT - Android API in commonMain
expect fun getContext(): Context  // Context is Android-only!
```
β†’ Flag: "Android API in commonMain won't compile on other platforms"

**Duplicated business logic:**
```kotlin
// ❌ INCORRECT - Same logic in both
// androidMain/.../CryptoUtils.kt
fun validateSignature(...) { ... }

// jvmMain/.../CryptoUtils.kt
fun validateSignature(...) { ... }  // Duplicated!
```
β†’ Flag: "Business logic duplicated, should be in commonMain or expect/actual"

**Reinventing wheel - suggest KMP alternatives:**
- Custom date/time β†’ kotlinx.datetime
- OkHttp β†’ ktor (supports web)
- Jackson β†’ kotlinx.serialization
- Custom UUID β†’ kotlinx.uuid (when stable)

## Common Pitfalls

### 1. Over-Abstraction
**Problem:** Creating expect/actual for UI components
```kotlin
// ❌ BAD
expect fun NavigationComponent(...)
```
**Why:** Navigation paradigms too different (Activity vs Window)
**Fix:** Keep platform-specific, accept duplication

### 2. Under-Sharing
**Problem:** Duplicating business logic across platforms
```kotlin
// ❌ BAD - duplicated in androidMain and jvmMain
fun parseNostrEvent(json: String): Event { ... }
```
**Why:** Bug fixes need to be applied twice, tests duplicated
**Fix:** Move to commonMain (pure Kotlin) or create expect/actual

### 3. Leaky Abstractions
**Problem:** Platform code in commonMain
```kotlin
// commonMain - ❌ BAD
import android.content.Context  // Won't compile on iOS!
```
**Fix:** Use expect/actual or dependency injection

### 4. Premature Abstraction
**Problem:** Creating expect/actual before second platform needs it
```kotlin
// ❌ BAD - only used on Android currently
expect fun showNotification(...)
```
**Why:** Wrong abstraction boundaries, wasted effort
**Fix:** Wait until iOS actually needs it, then abstract

### 5. Wrong Source Set
**Problem:** JVM libraries in commonMain
```kotlin
// commonMain - ❌ BAD
import com.fasterxml.jackson.databind.ObjectMapper
```
**Why:** Jackson won't compile on iOS/web
**Fix:** Move to jvmAndroid or migrate to kotlinx.serialization

## Quick Reference

| Code Type | Recommended Location | Reason |
|-----------|---------------------|--------|
| Pure Kotlin business logic | commonMain | Works everywhere |
| Nostr protocol, NIPs | commonMain | Core logic, no platform APIs |
| JVM libs (Jackson, OkHttp) | jvmAndroid | Android + Desktop only |
| Crypto (varies by platform) | expect in commonMain, actual in platforms | Different security APIs per platform |
| I/O, logging | expect in commonMain, actual in platforms | Platform implementations differ |
| State (business logic) | commonMain or commons/jvmAndroid | Reusable StateFlow patterns |
| **ViewModels** | **commons/commonMain/viewmodels/** | **StateFlow/SharedFlow + logic shareable, Compose MP lifecycle compatible** |
| UI formatters (pure) | commons/commonMain | Reusable, no dependencies |
| UI components (simple) | commons/commonMain | Cards, buttons, dialogs |
| **Screen layouts** | **Platform-specific** | **Window vs Activity, sidebar vs bottom nav** |
| Navigation | Platform-specific only | Activity vs Window too different |
| Permissions | Platform-specific only | APIs incompatible |
| Platform UX (menus, etc.) | Platform-specific only | Native feel required |

## See Also

- [references/abstraction-examples.md](references/abstraction-examples.md) - Good/bad abstraction examples with rationale
- [references/source-set-hierarchy.md](references/source-set-hierarchy.md) - Visual hierarchy with Amethyst examples
- [references/expect-actual-catalog.md](references/expect-actual-catalog.md) - All 24 expect/actual pairs with "why abstracted"
- [references/target-compatibility.md](references/target-compatibility.md) - Platform constraints and future-proofing

## Scripts

- `scripts/validate-kmp-structure.sh` - Detect incorrect placements, validate source sets
- `scripts/suggest-kmp-dependency.sh` - Suggest KMP library alternatives (ktor, kotlinx.serialization, etc.)

Quoted from vitorpamplona/amethyst for reference β€” see the original for the authoritative, latest version.

How to Use This Skill Unit

Option A: Project-Specific (Recommended)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/kotlin-multiplatform/
  3. Save the file as SKILL.md
  4. The agent will automatically discover the skill based on its description.

Option B: Global Installation (All Agents)

Save the file to these locations to make it available across all projects:

  • Claude Code: ~/.claude/skills/vitorpamplona/amethyst/kotlin-multiplatform/SKILL.md
  • Cursor: ~/.cursor/skills/vitorpamplona/amethyst/kotlin-multiplatform/SKILL.md
  • Antigravity: ~/.gemini/antigravity/skills/vitorpamplona/amethyst/kotlin-multiplatform/SKILL.md

πŸš€ Install with CLI:
npx skills add vitorpamplona/amethyst

Read the Master Guide: Mastering Agent Skills β†’

Recommended Rules

View more rules β†’

Recommended Workflows

View more workflows β†’

Recommended MCP Servers

View more MCP servers β†’

Take It Further

Maximize your productivity with these powerful resources

πŸ“‹

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
πŸ“–

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid architecture & design patterns issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under Architecture & Design Patterns and is published by vitorpamplona, maintained in vitorpamplona/amethyst.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.