Mobile Kotlin Multiplatform
Purpose
Guide for building Kotlin Multiplatform mobile apps with shared business logic, Compose Multiplatform UI, and platform-specific integrations.
Agent Protocol
Trigger
Phrases: "Kotlin Multiplatform", "KMP", "Compose Multiplatform", "shared Kotlin", "KMP module", "expect/actual", "commonMain", "KMP project", "multiplatform library"
Input Context
- Module structure (commonMain, androidMain, iosMain paths)
- Build files (build.gradle.kts with KMP plugin)
- Shared domain models and interfaces
- Platform-specific implementations
Output Artifact
Working KMP module with: commonMain business logic, expect/actual declarations, Compose Multiplatform screens, Gradle multi-module build configuration.
Response Format
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.
Completion Criteria
- commonMain compiles without platform imports
- expect/actual pairs resolve for all target platforms
- Compose Multiplatform screens render on Android and iOS
- Ktor client calls succeed on all platforms
- SQLDelight queries work cross-platform
Max Response Length
8000 tokens
Architecture Decision Trees
UI Strategy
Need shared UI?
├── Yes → Compose Multiplatform
│ Pros: Single UI codebase, Material3 theming, navigation
│ Cons: Cannot render native components (Map, Camera, WebView)
├── Native UI per platform
│ Pros: Full native API access, platform-native UX
│ Cons: Two UI codebases, more maintenance
└── Hybrid → Compose Multiplatform + expect/actual for native components
Strategy: 80% shared UI via Compose, 20% platform composable via expect
Networking Strategy
API complexity?
├── REST + JSON → Ktor Client + kotlinx.serialization
│ Engine: OkHttp (Android), Darwin (iOS)
├── GraphQL → Apollo Kotlin (KMP support)
└── gRPC → KMP-gRPC (emerging, check compatibility)
Persistence Strategy
Data model complexity?
├── Relational (SQL, joins) → SQLDelight
│ Common schema, platform drivers, Flow support
├── Key-value (settings, preferences) → multiplatform-settings
│ Wraps SharedPreferences (Android), NSUserDefaults (iOS)
├── NoSQL/Document → Realm Kotlin SDK
│ KMP-native, reactive, synchronization
└── Encrypted → SQLCipher (SQLDelight cipher) or platform Keychain/Keystore
Dependency Injection
DI framework preference?
├── Lightweight, KMP-native → Koin
│ No code gen, easy setup, reasonable runtime performance
├── Compile-time verified → Anvil (Dagger-based)
│ Works with KMP, faster than Kapt
└── Manual → Constructor injection with factory pattern
Workflow
KMP project structure — Three-tier source set layout: commonMain (shared business logic, domain models, repository interfaces, Ktor client, SQLDelight schema, expect declarations), androidMain (Android-specific actual implementations, OkHttp engine, Android Sqlite driver, Context-dependent factories), iosMain (iOS-specific actual implementations, Darwin engine, NativeSqlite driver, platform factories). Additional source sets for testing: commonTest, androidUnitTest, iosTest. The shared module is consumed by Android apps as an AAR library and by iOS apps as a Kotlin/Native framework.
expect/actual pattern — Declare platform-agnostic interfaces in commonMain using expect keyword. Provide concrete implementations in platform source sets with actual. Three forms: expect fun (function), expect class (class with actual constructors), expect object (singleton), expect val (property), expect typealias (type alias for platform types). Example: expect fun generateUuid(): String with actual fun generateUuid(): String = UUID.randomUUID().toString() in Android and actual fun generateUuid(): String = platform.Foundation.NSUUID().UUIDString() in iOS. Keep expect declarations in commonMain/.../platform/ package.
Ktor client configuration — Single HttpClient declaration in commonMain with engine-agnostic setup. Use ktor-client-core for common code, ktor-client-okhttp for Android (supports HTTP/2, caching), ktor-client-darwin for iOS (uses NSURLSession, automatic cookie storage). Configure serialization with kotlinx-serialization-json using ContentNegotiation plugin. Add logging with Logging plugin. Timeout configuration: HttpTimeout plugin with requestTimeoutMillis, connectTimeoutMillis, socketTimeoutMillis.
// commonMain — Ktor client factory
val httpClient = HttpClient {
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
install(Logging) { level = LogLevel.HEADERS }
install(HttpTimeout) { requestTimeoutMillis = 15_000 }
defaultRequest { url("https://api.example.com/") }
}
kotlinx.serialization — All data classes in commonMain use @Serializable annotation. Supports primitives, enums, sealed classes, nullable fields, default values. Custom serializers for non-standard types (Date, BigInteger). Json configuration: ignoreUnknownKeys = true for forward compatibility, coerceInputValues = true for invalid defaults, encodeDefaults = false to minimize payload. Use @SerialName for property name mapping. Polymorphic serialization for sealed class hierarchies.
Compose Multiplatform UI — All shared UI in commonMain using Jetpack Compose APIs. The org.jetbrains.compose plugin compiles Compose code for both platforms. Material3 theming with MaterialTheme — customize typography, color scheme, and shapes. Navigation options: Voyager (screen-based, type-safe), Decompose (component-based, lifecycle-aware), or custom state-driven navigation. Platform-specific composables via expect/actual for views that cannot be shared (maps, WebView, Camera). Performance: remember for expensive computations, derivedStateOf for computed state, LaunchedEffect for side effects.
Platform-specific UI integration — When Compose Multiplatform cannot render a native component, use expect composable functions. Android: wrap Android Views via AndroidView composable factory. iOS: wrap UIKit views via UIKitView composable (KMP Compose provides interop). Example: MapView, CameraPreview, WebView, NativeTextInput. Keep platform composables thin — minimal wrapper code, pass data via parameters. Platform UI code lives in androidMain/iosMain Composable files.
Testing strategy — commonTest for shared business logic: domain models, repository logic, ViewModel state. Use kotlin.test for assertions. Mock dependencies with mock libraries that support KMP (MockK, KMM- Mock). Instrumented tests on Android and iOS use platform source sets. UI testing of Compose screens uses Compose UI Test framework in commonTest. Run iOS tests on simulator via Gradle task or Xcode Test Navigator.
Platform Compatibility
| Feature |
commonMain |
androidMain |
iosMain |
| Business logic |
Full |
Thin override |
Thin override |
| HTTP client |
Ktor declaration |
OkHttp engine |
Darwin engine |
| Database |
SQLDelight schema |
Android driver |
Native driver |
| UI (Compose) |
Full |
Full |
Full |
| Platform APIs |
expect decl |
actual impl |
actual impl |
| Dependency injection |
Koin module decl |
platform bindings |
platform bindings |
Best Practices
- Keep platform code thin — actual implementations should be 1-5 lines wrapping platform APIs
- Use expect/actual for factory functions, not for large service classes
- Prefer interface-based abstractions over expect/actual for testability
- Use Ktor engine selection at compile time, never at runtime
- Version all shared dependencies in a single
libs.versions.toml catalog
- Run
./gradlew allTests before committing to verify all targets compile
- Use
kotlinx.datetime for cross-platform date/time handling
Common Pitfalls
- No android. imports in commonMain*: The compiler enforces this, but watch for transitive dependencies that pull Android types.
- iOS framework linking: Ensure
embedAndSignAppleFrameworkForXcode is in the build phase of your Xcode project.
- Serialization class clashes: Two modules with the same
@Serializable class cause linker errors. Use explicit @SerialName or module-level serializers.
- Generic type erasure:
expect/actual with generics requires @Suppress("NO_ACTUAL_FOR_EXPECT") in some cases.
- CocoaPods vs SPM: CocoaPods + KMP is more mature than SPM integration. Prefer CocoaPods for iOS dependency distribution.
Anti-Patterns
- Fat platform source sets: If androidMain has more than 10 files, you're not sharing enough — refactor to commonMain
- expect/actual for everything: Interfaces with platform implementations are more testable than expect/actual
- Ignoring iOS concurrency model: Kotlin coroutines on iOS use custom dispatch — test iOS-specific threading scenarios
- No commonTest coverage: If commonTest is empty, you lose the main advantage of KMP — shared test coverage
- Manual memory management on iOS: Kotlin/Native uses ARC — but watch for cyclic references between Kotlin and Swift objects
- Outdated libs.versions.toml: KMP ecosystem moves fast — update Ktor, Kotlin, Compose versions together
- Mixing KMP modules with Android-only dependencies: Keep KMP modules pure — put Android UI in separate :app module
Build & Deployment Patterns
CI/CD for KMP Projects
KMP requires building for multiple targets — Android (JVM) and iOS (Kotlin/Native). CI must handle both environments. Recommended CI matrix:
# GitHub Actions example
jobs:
android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with: { distribution: 'temurin', java-version: '17' }
- name: Build Android
run: ./gradlew :shared:assembleAndroidDebug :app:assembleDebug
- name: Run common tests
run: ./gradlew :shared:allTests
ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with: { distribution: 'temurin', java-version: '17' }
- name: Build iOS framework
run: |
./gradlew :shared:linkDebugFrameworkIosSimulatorArm64
./gradlew :shared:linkDebugFrameworkIosArm64
- name: Run iOS tests
run: ./gradlew :shared:iosX64Test :shared:iosSimulatorArm64Test
- name: Build Xcode project
run: |
cd iosApp
xcodebuild -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15'
iOS Framework Integration
The KMP shared module produces a Kotlin/Native framework consumed by Xcode:
Gradle setup: Configure framework name and target in shared/build.gradle.kts:
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework {
baseName = "shared"
isStatic = true
export("my-exported-module") // re-export module's API
}
}
Xcode integration: Add build phase to embed framework:
- In Xcode target → Build Phases → New Run Script Phase
- Script:
cd "$SRCROOT/.." && ./gradlew :shared:embedAndSignAppleFrameworkForXcode
- Input files:
$(SRCROOT)/../shared/build/bin/iosSimulatorArm64/debugFramework/shared.framework
SPM/CocoaPods distribution: Publish framework as CocoaPod:
# shared.podspec
Pod::Spec.new do |spec|
spec.name = 'Shared'
spec.vendored_frameworks = 'shared.framework'
spec.platform = :ios, '16.0'
end
Version Catalog (libs.versions.toml)
[versions]
kotlin = "2.0.21"
ktor = "3.0.3"
sqldelight = "2.0.2"
compose-multiplatform = "1.7.1"
koin = "3.5.6"
coroutines = "1.9.0"
datetime = "0.6.1"
[libraries]
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
sqldelight-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
Gradle Multi-Module Structure
project/
├── shared/ # KMP shared module
│ ├── src/commonMain/
│ ├── src/androidMain/
│ ├── src/iosMain/
│ └── build.gradle.kts
├── composeApp/ # Compose Multiplatform UI module
│ ├── src/commonMain/
│ ├── src/androidMain/
│ ├── src/iosMain/
│ └── build.gradle.kts
├── app/ # Android shell app (only if not using composeApp)
│ └── src/main/
├── iosApp/ # iOS Xcode project
│ └── iosApp.xcodeproj/
└── build.gradle.kts
Keep shared module pure — no Android UI dependencies. The composeApp module depends on shared and provides the Compose UI layer. Android app module wraps composeApp; iOS app embeds the framework from composeApp or shared.
Performance Optimization
Compose Multiplatform Rendering
remember and derivedStateOf: Cache expensive computations. val total = remember(items) { items.sumOf { it.price } } recalculates only when items change. Use derivedStateOf for computed state that derives from other state.
LaunchedEffect lifecycle: Runs in the composition's coroutine scope. Cancelled when composable leaves composition — no manual cleanup needed for coroutines. Use DisposableEffect for resources that need cleanup (listeners, observers).
Modifier ordering: Order matters — Modifier.clip().size(100.dp).background(Color.Red) clips before sizing, which may not work as expected. Always clip after sizing: Modifier.size(100.dp).clip(CircleShape).background(Color.Red).
- Lambda stability: Use
remember for lambdas passed to composables: val { { handleClick() } }. Prevents unnecessary recomposition of child composables that receive unstable lambdas.
key parameter in LazyColumn: LazyColumn { items(items, key = { it.id }) } provides stable identity — enables item animation, preserves scroll position across recomposition, and minimizes recomposition scope.
contentType in lazy lists: When LazyColumn has mixed item types (headers, items, footers), set contentType parameter: items(items, contentType = { "item" }). Helps the layout engine optimize recycling.
Memory and Allocation
kotlin.Result zero-cost: Use Result<T> for operation outcomes instead of sealed classes in performance-critical paths — the Kotlin compiler optimizes it as a single object.
- Primitive collections: Use
IntArray, FloatArray, LongArray over List<Int>, List<Float> for numeric-heavy operations. Reduces boxing overhead 10-20x.
@Immutable / @Stable annotations: Mark data classes as @Immutable (all properties final, never change) or @Stable (changes are reported to Compose). This enables Compose's compiler to skip recomposition when state hasn't changed. Without these annotations, Compose pessimistically recomposes.
- Avoid
var in state holders: Use val with MutableState/MutableStateFlow. var customerName by remember { mutableStateOf("") } — the by delegate enables automatic recomposition. Mutating var directly (without delegation) doesn't trigger recomposition.
buildList / buildMap / buildString: Allocation-efficient collection builders. buildList { addAll(items); sort() } creates a single list at the end, not intermediate copies for each operation.
- Image loading: Coil 3 (KMP-compatible) with Compose integration. Use
AsyncImage with ImageRequest.Builder for disk/memory cache, transform pipelines, and placeholder/error states.
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("https://example.com/image.jpg")
.crossfade(true)
.size(512, 512)
.build(),
contentDescription = "Product image",
modifier = Modifier.clip(RoundedCornerShape(8.dp))
)
Ktor Client Optimization
- Connection pooling: Configure
HttpClient with custom engine options. OkHttp: OkHttp.config { connectionPool(ConnectionPool(5, 30, TimeUnit.SECONDS)) }. Darwin: Darwin.config { configureRequest { setAllowsCellularAccess(true) } }.
- Response caching: Enable OkHttp cache on Android for GET requests. Configure cache directory and max size. Reduces redundant network calls.
- Serialization performance: Use
kotlinx.serialization with Json { ignoreUnknownKeys = true; isLenient = true } over Gson or Moshi. KMP-native serialization is 2-3x faster than reflection-based alternatives.
- WebSocket keep-alive: For real-time connections, configure
WebSockets { pingInterval = 30_000 } — sends ping frames every 30s to keep the connection alive and detect disconnects early.
Architecture Patterns (Expanded)
Repository Pattern with Caching
// commonMain
class OrderRepository(
private val api: OrderApi,
private val db: OrderDatabase,
) {
fun getOrders(): Flow<List<Order>> = flow {
// 1. Emit cached first
val cached = db.orderQueries.selectAll().executeAsList()
if (cached.isNotEmpty()) emit(cached.map { it.toOrder() })
// 2. Fetch fresh from network
val fresh = api.getOrders()
db.transaction {
db.orderQueries.deleteAll()
fresh.forEach { db.orderQueries.insert(it.toEntity()) }
}
// 3. Emit fresh
emit(fresh)
}
}
Clean Architecture Module Layers
shared/src/commonMain/kotlin/com/app/
├── domain/ # Pure Kotlin, no framework dependencies
│ ├── model/ # Domain entities (data classes)
│ ├── repository/ # Repository interfaces
│ └── usecase/ # Business logic (single responsibility)
├── data/ # Implements domain interfaces
│ ├── remote/ # Ktor API clients
│ ├── local/ # SQLDelight DAOs
│ └── repository/ # Repository implementations
├── di/ # Koin module definitions
└── platform/ # expect declarations
└── Platform.kt
Domain layer has zero dependencies on Ktor, SQLDelight, or Compose. This makes it testable in commonTest without platform setup. Repository interfaces are in domain (OrderRepository), implementations are in data (OrderRepositoryImpl).
Sealed Class State Management
sealed interface UiState<out T> {
data object Loading : UiState<Nothing>
data class Success<T>(val data: T) : UiState<T>
data class Error(val message: String, val throwable: Throwable?) : UiState<Nothing>
}
@Composable
fun <T> ContentView(
state: UiState<T>,
onRetry: () -> Unit,
content: @Composable (T) -> Unit
) {
when (state) {
is UiState.Loading -> ShimmerLoading()
is UiState.Success -> content(state.data)
is UiState.Error -> ErrorView(state.message, onRetry)
}
}
Anti-Patterns (Expanded)
- Sharing platform-specific types:
java.io.File in commonMain breaks compilation. Use expect/actual or interfaces for platform types.
- One-shot expect/actual for everything: Each
expect declaration needs actual on every platform — increases maintenance. Prefer interfaces with platform DI.
- Blocking main thread on iOS: Kotlin coroutines on iOS may dispatch to the main thread. Use
Dispatchers.Main.immediate with withContext for UI updates. Never use runBlocking on main thread in iOS.
- No iOS memory optimization: Kotlin/Native frameworks shipped with debug symbols by default. Strip with
isStatic = true and set embedAndSignAppleFrameworkForXcode to use release builds in production.
kotlin.test vs platform test runners: commonTest uses kotlin.test assertions, which differ from JUnit 5 and XCTest. Teams must learn KMP test patterns.
- Over-reliance on Compose for everything: Compose Multiplatform can't render native MapView, CameraPreview, or ARKit/ARCore. For 20% of features that need native components, use
expect composable wrappers and keep them thin.
- Mixing CocoaPods and SPM: Choose one dependency manager for iOS. CocoaPods has better KMP integration. SPM support is improving but still has edge cases with transitive dependencies.
- Missing
@ThreadLocal on iOS objects: iOS object initializers run on arbitrary threads. Use @ThreadLocal annotation or AtomicReference for mutable state accessed from multiple threads.
Configuration Reference
// build.gradle.kts (shared module)
plugins {
id("org.jetbrains.kotlin.multiplatform") version "2.0.21"
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.21"
id("org.jetbrains.compose") version "1.7.1"
id("app.cash.sqldelight") version "2.0.2"
}
kotlin {
androidTarget { compilations.all { kotlinOptions { jvmTarget = "17" } } }
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework { baseName = "shared"; isStatic = true }
}
sourceSets {
commonMain.dependencies {
implementation("io.ktor:ktor-client-core:3.0.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation("app.cash.sqldelight:runtime:2.0.2")
implementation(compose.runtime); implementation(compose.foundation)
implementation(compose.material3); implementation(compose.ui)
}
androidMain.dependencies {
implementation("io.ktor:ktor-client-okhttp:3.0.3")
implementation("app.cash.sqldelight:android-driver:2.0.2")
}
iosMain.dependencies {
implementation("io.ktor:ktor-client-darwin:3.0.3")
implementation("app.cash.sqldelight:native-driver:2.0.2")
}
}
}
References
- references/kmm-concurrency.md — KMM Concurrency — Coroutines, Flows, and Threading
- references/kmm-networking.md — KMM Networking — Ktor, SQLDelight, Serialization
- references/kmp-compose.md — Compose Multiplatform
- references/kmp-structure.md — KMP Module Structure
- references/kotlin-multiplatform-advanced.md — Kotlin Multiplatform Advanced Topics
- references/kotlin-multiplatform-fundamentals.md — Kotlin Multiplatform Fundamentals
- references/platform-specific.md — Platform-Specific Implementations
Handoff
Hand off to platform-specific iOS or Android skills when expect/actual implementations need deep platform API knowledge.
Implementation Patterns
Observer Pattern for Event Handling
`
interface EventObserver {
onEvent(event: T): Promise;
}
class EventBus {
private observers: Set<EventObserver> = new Set();
subscribe(observer: EventObserver): void {
this.observers.add(observer);
}
unsubscribe(observer: EventObserver): void {
this.observers.delete(observer);
}
async emit(event: T): Promise {
const results = Array.from(this.observers).map(o => o.onEvent(event));
await Promise.allSettled(results);
}
}
`
Configuration-Driven Approach
config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1
Production Considerations
Deployment Checklist
Monitoring and Alerting
| Metric |
Threshold |
Severity |
Action |
| Error rate |
> 1% over 5min |
Critical |
Page on-call |
| p99 latency |
> 2s over 5min |
Warning |
Investigate |
| Throughput drop |
> 50% over 1min |
Critical |
Check upstream |
| Queue depth |
> 1000 over 1min |
Warning |
Scale consumers |
| Disk usage |
> 85% |
Warning |
Clean or expand |
| Memory usage |
> 90% heap |
Critical |
Restart or scale |
Anti-Patterns
| Anti-Pattern |
Symptom |
Root Cause |
Solution |
| Premature optimization |
Complex code for no measured benefit |
Guessing instead of profiling |
Measure first, optimize based on data |
| Copy-paste reuse |
Duplicate code across codebase |
Lack of abstraction |
Extract shared logic into libraries |
| Gold-plating |
Features with no current requirement |
Over-engineering |
YAGNI — build what's needed now |
| Magical thinking |
Assumptions without validation |
Skipping error handling |
Handle all failure modes explicitly |
Performance Optimization
Caching Strategy
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge).
Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).
Resource Pooling
- Database connections: Pool of reusable connections (HikariCP, pgBouncer)
- HTTP connections: Keep-alive + connection pooling for external calls
- Thread pool: Bounded thread pools for async task execution
Profiling Methodology
- Establish baseline with production traffic profile
- Profile CPU with sampling profiler (pprof, perf, async-profiler)
- Profile memory with heap dumps and allocation tracking
- Profile I/O with strace/perf trace for syscall analysis
- Profile latency with distributed tracing (OpenTelemetry)
- Identify bottleneck, formulate hypothesis, implement fix
- Re-profile to verify improvement, repeat
Security Considerations
Threat Modeling (STRIDE)
- Spoofing: Identity validation, authentication
- Tampering: Integrity checks, digital signatures
- Repudiation: Audit logs, non-repudiation
- Information disclosure: Encryption, access control
- Denial of service: Rate limiting, resource quotas
- Elevation of privilege: Principle of least privilege
Supply Chain Security
- Dependency scanning: Snyk, Dependabot, Trivy
- SBOM generation: CycloneDX or SPDX format
- Signed commits: GPG or SSH commit signing
- Artifact verification: Checksum validation, signature verification
Secrets Management
- Secrets never in code — always in secrets manager (Vault, AWS Secrets Manager)
- Rotation policy: Rotate database credentials every 90 days
- Access audit: Log every secrets access, alert on anomalies
- Encryption at rest and in transit for all secrets
- Principle of least privilege: each service gets only its own secrets
Rules
- Default-deny security posture — allow only explicitly required access.
- All inputs validated, all outputs encoded, all errors handled.
- Defend in depth — multiple layers of security controls.
- Fail securely — errors default to safe behavior.
- Log security-relevant events for audit and investigation.
- Keep dependencies updated — automate vulnerability scanning.
- Design for observability from day one, not as an afterthought.
- Document all architectural decisions with rationale.
- Review code for security, performance, and correctness before merging.
1---2name: mobile-kotlin-multiplatform3description: Use this skill when the user says 'Kotlin Multiplatform', 'KMP', 'Compose Multiplatform', 'shared Kotlin', 'KMP module', 'expect/actual', 'commonMain', 'KMP project', 'multiplatform library'. Build cross-platform mobile apps with Kotlin Multiplatform sharing business logic, Compose Multiplatform UI, and platform-specific integrations. Do NOT use for: Android-only or iOS-only app development.4license: MIT5---67# Mobile Kotlin Multiplatform89## Purpose10Guide for building Kotlin Multiplatform mobile apps with shared business logic, Compose Multiplatform UI, and platform-specific integrations.1112## Agent Protocol1314### Trigger15Phrases: "Kotlin Multiplatform", "KMP", "Compose Multiplatform", "shared Kotlin", "KMP module", "expect/actual", "commonMain", "KMP project", "multiplatform library"1617### Input Context18- Module structure (commonMain, androidMain, iosMain paths)19- Build files (build.gradle.kts with KMP plugin)20- Shared domain models and interfaces21- Platform-specific implementations2223### Output Artifact24Working KMP module with: commonMain business logic, expect/actual declarations, Compose Multiplatform screens, Gradle multi-module build configuration.2526### Response Format27No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.2829### Completion Criteria30- commonMain compiles without platform imports31- expect/actual pairs resolve for all target platforms32- Compose Multiplatform screens render on Android and iOS33- Ktor client calls succeed on all platforms34- SQLDelight queries work cross-platform3536### Max Response Length378000 tokens3839## Architecture Decision Trees4041### UI Strategy42```43Need shared UI?44├── Yes → Compose Multiplatform45│ Pros: Single UI codebase, Material3 theming, navigation46│ Cons: Cannot render native components (Map, Camera, WebView)47├── Native UI per platform48│ Pros: Full native API access, platform-native UX49│ Cons: Two UI codebases, more maintenance50└── Hybrid → Compose Multiplatform + expect/actual for native components51 Strategy: 80% shared UI via Compose, 20% platform composable via expect52```5354### Networking Strategy55```56API complexity?57├── REST + JSON → Ktor Client + kotlinx.serialization58│ Engine: OkHttp (Android), Darwin (iOS)59├── GraphQL → Apollo Kotlin (KMP support)60└── gRPC → KMP-gRPC (emerging, check compatibility)61```6263### Persistence Strategy64```65Data model complexity?66├── Relational (SQL, joins) → SQLDelight67│ Common schema, platform drivers, Flow support68├── Key-value (settings, preferences) → multiplatform-settings69│ Wraps SharedPreferences (Android), NSUserDefaults (iOS)70├── NoSQL/Document → Realm Kotlin SDK71│ KMP-native, reactive, synchronization72└── Encrypted → SQLCipher (SQLDelight cipher) or platform Keychain/Keystore73```7475### Dependency Injection76```77DI framework preference?78├── Lightweight, KMP-native → Koin79│ No code gen, easy setup, reasonable runtime performance80├── Compile-time verified → Anvil (Dagger-based)81│ Works with KMP, faster than Kapt82└── Manual → Constructor injection with factory pattern83```8485## Workflow86871. **KMP project structure** — Three-tier source set layout: `commonMain` (shared business logic, domain models, repository interfaces, Ktor client, SQLDelight schema, expect declarations), `androidMain` (Android-specific actual implementations, OkHttp engine, Android Sqlite driver, Context-dependent factories), `iosMain` (iOS-specific actual implementations, Darwin engine, NativeSqlite driver, platform factories). Additional source sets for testing: `commonTest`, `androidUnitTest`, `iosTest`. The shared module is consumed by Android apps as an AAR library and by iOS apps as a Kotlin/Native framework.88892. **expect/actual pattern** — Declare platform-agnostic interfaces in `commonMain` using `expect` keyword. Provide concrete implementations in platform source sets with `actual`. Three forms: `expect fun` (function), `expect class` (class with actual constructors), `expect object` (singleton), `expect val` (property), `expect typealias` (type alias for platform types). Example: `expect fun generateUuid(): String` with `actual fun generateUuid(): String = UUID.randomUUID().toString()` in Android and `actual fun generateUuid(): String = platform.Foundation.NSUUID().UUIDString()` in iOS. Keep expect declarations in `commonMain/.../platform/` package.90913. **Ktor client configuration** — Single `HttpClient` declaration in `commonMain` with engine-agnostic setup. Use `ktor-client-core` for common code, `ktor-client-okhttp` for Android (supports HTTP/2, caching), `ktor-client-darwin` for iOS (uses NSURLSession, automatic cookie storage). Configure serialization with `kotlinx-serialization-json` using `ContentNegotiation` plugin. Add logging with `Logging` plugin. Timeout configuration: `HttpTimeout` plugin with `requestTimeoutMillis`, `connectTimeoutMillis`, `socketTimeoutMillis`.9293```kotlin94// commonMain — Ktor client factory95val httpClient = HttpClient {96 install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }97 install(Logging) { level = LogLevel.HEADERS }98 install(HttpTimeout) { requestTimeoutMillis = 15_000 }99 defaultRequest { url("https://api.example.com/") }100}101```1021034. **kotlinx.serialization** — All data classes in `commonMain` use `@Serializable` annotation. Supports primitives, enums, sealed classes, nullable fields, default values. Custom serializers for non-standard types (Date, BigInteger). `Json` configuration: `ignoreUnknownKeys = true` for forward compatibility, `coerceInputValues = true` for invalid defaults, `encodeDefaults = false` to minimize payload. Use `@SerialName` for property name mapping. Polymorphic serialization for sealed class hierarchies.1041055. **Compose Multiplatform UI** — All shared UI in `commonMain` using Jetpack Compose APIs. The `org.jetbrains.compose` plugin compiles Compose code for both platforms. Material3 theming with `MaterialTheme` — customize typography, color scheme, and shapes. Navigation options: Voyager (screen-based, type-safe), Decompose (component-based, lifecycle-aware), or custom state-driven navigation. Platform-specific composables via `expect`/`actual` for views that cannot be shared (maps, WebView, Camera). Performance: `remember` for expensive computations, `derivedStateOf` for computed state, `LaunchedEffect` for side effects.1061076. **Platform-specific UI integration** — When Compose Multiplatform cannot render a native component, use `expect` composable functions. Android: wrap Android Views via `AndroidView` composable factory. iOS: wrap UIKit views via `UIKitView` composable (KMP Compose provides interop). Example: MapView, CameraPreview, WebView, NativeTextInput. Keep platform composables thin — minimal wrapper code, pass data via parameters. Platform UI code lives in `androidMain`/`iosMain` Composable files.1081097. **Testing strategy** — `commonTest` for shared business logic: domain models, repository logic, ViewModel state. Use kotlin.test for assertions. Mock dependencies with mock libraries that support KMP (MockK, KMM- Mock). Instrumented tests on Android and iOS use platform source sets. UI testing of Compose screens uses Compose UI Test framework in commonTest. Run iOS tests on simulator via Gradle task or Xcode Test Navigator.110111## Platform Compatibility112113| Feature | commonMain | androidMain | iosMain |114|---------|-----------|-------------|---------|115| Business logic | Full | Thin override | Thin override |116| HTTP client | Ktor declaration | OkHttp engine | Darwin engine |117| Database | SQLDelight schema | Android driver | Native driver |118| UI (Compose) | Full | Full | Full |119| Platform APIs | expect decl | actual impl | actual impl |120| Dependency injection | Koin module decl | platform bindings | platform bindings |121122## Best Practices123124- Keep platform code thin — actual implementations should be 1-5 lines wrapping platform APIs125- Use expect/actual for factory functions, not for large service classes126- Prefer interface-based abstractions over expect/actual for testability127- Use Ktor engine selection at compile time, never at runtime128- Version all shared dependencies in a single `libs.versions.toml` catalog129- Run `./gradlew allTests` before committing to verify all targets compile130- Use `kotlinx.datetime` for cross-platform date/time handling131132## Common Pitfalls133134- **No android.* imports in commonMain**: The compiler enforces this, but watch for transitive dependencies that pull Android types.135- **iOS framework linking**: Ensure `embedAndSignAppleFrameworkForXcode` is in the build phase of your Xcode project.136- **Serialization class clashes**: Two modules with the same `@Serializable` class cause linker errors. Use explicit `@SerialName` or module-level serializers.137- **Generic type erasure**: `expect`/`actual` with generics requires `@Suppress("NO_ACTUAL_FOR_EXPECT")` in some cases.138- **CocoaPods vs SPM**: CocoaPods + KMP is more mature than SPM integration. Prefer CocoaPods for iOS dependency distribution.139140## Anti-Patterns141142- **Fat platform source sets**: If androidMain has more than 10 files, you're not sharing enough — refactor to commonMain143- **expect/actual for everything**: Interfaces with platform implementations are more testable than expect/actual144- **Ignoring iOS concurrency model**: Kotlin coroutines on iOS use custom dispatch — test iOS-specific threading scenarios145- **No commonTest coverage**: If commonTest is empty, you lose the main advantage of KMP — shared test coverage146- **Manual memory management on iOS**: Kotlin/Native uses ARC — but watch for cyclic references between Kotlin and Swift objects147- **Outdated libs.versions.toml**: KMP ecosystem moves fast — update Ktor, Kotlin, Compose versions together148- **Mixing KMP modules with Android-only dependencies**: Keep KMP modules pure — put Android UI in separate :app module149150## Build & Deployment Patterns151152### CI/CD for KMP Projects153154KMP requires building for multiple targets — Android (JVM) and iOS (Kotlin/Native). CI must handle both environments. Recommended CI matrix:155156```yaml157# GitHub Actions example158jobs:159 android:160 runs-on: ubuntu-latest161 steps:162 - uses: actions/checkout@v4163 - name: Set up JDK 17164 uses: actions/setup-java@v4165 with: { distribution: 'temurin', java-version: '17' }166 - name: Build Android167 run: ./gradlew :shared:assembleAndroidDebug :app:assembleDebug168 - name: Run common tests169 run: ./gradlew :shared:allTests170171 ios:172 runs-on: macos-latest173 steps:174 - uses: actions/checkout@v4175 - name: Set up JDK 17176 uses: actions/setup-java@v4177 with: { distribution: 'temurin', java-version: '17' }178 - name: Build iOS framework179 run: |180 ./gradlew :shared:linkDebugFrameworkIosSimulatorArm64181 ./gradlew :shared:linkDebugFrameworkIosArm64182 - name: Run iOS tests183 run: ./gradlew :shared:iosX64Test :shared:iosSimulatorArm64Test184 - name: Build Xcode project185 run: |186 cd iosApp187 xcodebuild -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15'188```189190### iOS Framework Integration191192The KMP shared module produces a Kotlin/Native framework consumed by Xcode:1931941. **Gradle setup**: Configure framework name and target in shared/build.gradle.kts:195 ```kotlin196 listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {197 it.binaries.framework {198 baseName = "shared"199 isStatic = true200 export("my-exported-module") // re-export module's API201 }202 }203 ```2042052. **Xcode integration**: Add build phase to embed framework:206 - In Xcode target → Build Phases → New Run Script Phase207 - Script: `cd "$SRCROOT/.." && ./gradlew :shared:embedAndSignAppleFrameworkForXcode`208 - Input files: `$(SRCROOT)/../shared/build/bin/iosSimulatorArm64/debugFramework/shared.framework`2092103. **SPM/CocoaPods distribution**: Publish framework as CocoaPod:211 ```ruby212 # shared.podspec213 Pod::Spec.new do |spec|214 spec.name = 'Shared'215 spec.vendored_frameworks = 'shared.framework'216 spec.platform = :ios, '16.0'217 end218 ```219220### Version Catalog (libs.versions.toml)221```toml222[versions]223kotlin = "2.0.21"224ktor = "3.0.3"225sqldelight = "2.0.2"226compose-multiplatform = "1.7.1"227koin = "3.5.6"228coroutines = "1.9.0"229datetime = "0.6.1"230231[libraries]232ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }233ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }234ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }235ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }236ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }237sqldelight-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }238sqldelight-native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" }239sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }240koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }241koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }242datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }243```244245### Gradle Multi-Module Structure246```247project/248├── shared/ # KMP shared module249│ ├── src/commonMain/250│ ├── src/androidMain/251│ ├── src/iosMain/252│ └── build.gradle.kts253├── composeApp/ # Compose Multiplatform UI module254│ ├── src/commonMain/255│ ├── src/androidMain/256│ ├── src/iosMain/257│ └── build.gradle.kts258├── app/ # Android shell app (only if not using composeApp)259│ └── src/main/260├── iosApp/ # iOS Xcode project261│ └── iosApp.xcodeproj/262└── build.gradle.kts263```264265Keep `shared` module pure — no Android UI dependencies. The `composeApp` module depends on `shared` and provides the Compose UI layer. Android app module wraps `composeApp`; iOS app embeds the framework from `composeApp` or `shared`.266267## Performance Optimization268269### Compose Multiplatform Rendering270271- **`remember` and `derivedStateOf`**: Cache expensive computations. `val total = remember(items) { items.sumOf { it.price } }` recalculates only when items change. Use `derivedStateOf` for computed state that derives from other state.272- **`LaunchedEffect` lifecycle**: Runs in the composition's coroutine scope. Cancelled when composable leaves composition — no manual cleanup needed for coroutines. Use `DisposableEffect` for resources that need cleanup (listeners, observers).273- **`Modifier` ordering**: Order matters — `Modifier.clip().size(100.dp).background(Color.Red)` clips before sizing, which may not work as expected. Always clip after sizing: `Modifier.size(100.dp).clip(CircleShape).background(Color.Red)`.274- **Lambda stability**: Use `remember` for lambdas passed to composables: `val onClick = remember { { handleClick() } }`. Prevents unnecessary recomposition of child composables that receive unstable lambdas.275- **`key` parameter in `LazyColumn`**: `LazyColumn { items(items, key = { it.id }) }` provides stable identity — enables item animation, preserves scroll position across recomposition, and minimizes recomposition scope.276- **`contentType` in lazy lists**: When `LazyColumn` has mixed item types (headers, items, footers), set `contentType` parameter: `items(items, contentType = { "item" })`. Helps the layout engine optimize recycling.277278### Memory and Allocation279280- **`kotlin.Result` zero-cost**: Use `Result<T>` for operation outcomes instead of sealed classes in performance-critical paths — the Kotlin compiler optimizes it as a single object.281- **Primitive collections**: Use `IntArray`, `FloatArray`, `LongArray` over `List<Int>`, `List<Float>` for numeric-heavy operations. Reduces boxing overhead 10-20x.282- **`@Immutable` / `@Stable` annotations**: Mark data classes as `@Immutable` (all properties final, never change) or `@Stable` (changes are reported to Compose). This enables Compose's compiler to skip recomposition when state hasn't changed. Without these annotations, Compose pessimistically recomposes.283- **Avoid `var` in state holders**: Use `val` with `MutableState`/`MutableStateFlow`. `var customerName by remember { mutableStateOf("") }` — the `by` delegate enables automatic recomposition. Mutating `var` directly (without delegation) doesn't trigger recomposition.284- **`buildList` / `buildMap` / `buildString`**: Allocation-efficient collection builders. `buildList { addAll(items); sort() }` creates a single list at the end, not intermediate copies for each operation.285- **Image loading**: Coil 3 (KMP-compatible) with Compose integration. Use `AsyncImage` with `ImageRequest.Builder` for disk/memory cache, transform pipelines, and placeholder/error states.286287```kotlin288AsyncImage(289 model = ImageRequest.Builder(LocalContext.current)290 .data("https://example.com/image.jpg")291 .crossfade(true)292 .size(512, 512)293 .build(),294 contentDescription = "Product image",295 modifier = Modifier.clip(RoundedCornerShape(8.dp))296)297```298299### Ktor Client Optimization300301- **Connection pooling**: Configure `HttpClient` with custom engine options. OkHttp: `OkHttp.config { connectionPool(ConnectionPool(5, 30, TimeUnit.SECONDS)) }`. Darwin: `Darwin.config { configureRequest { setAllowsCellularAccess(true) } }`.302- **Response caching**: Enable OkHttp cache on Android for GET requests. Configure cache directory and max size. Reduces redundant network calls.303- **Serialization performance**: Use `kotlinx.serialization` with `Json { ignoreUnknownKeys = true; isLenient = true }` over Gson or Moshi. KMP-native serialization is 2-3x faster than reflection-based alternatives.304- **WebSocket keep-alive**: For real-time connections, configure `WebSockets { pingInterval = 30_000 }` — sends ping frames every 30s to keep the connection alive and detect disconnects early.305306## Architecture Patterns (Expanded)307308### Repository Pattern with Caching309```kotlin310// commonMain311class OrderRepository(312 private val api: OrderApi,313 private val db: OrderDatabase,314) {315 fun getOrders(): Flow<List<Order>> = flow {316 // 1. Emit cached first317 val cached = db.orderQueries.selectAll().executeAsList()318 if (cached.isNotEmpty()) emit(cached.map { it.toOrder() })319320 // 2. Fetch fresh from network321 val fresh = api.getOrders()322 db.transaction {323 db.orderQueries.deleteAll()324 fresh.forEach { db.orderQueries.insert(it.toEntity()) }325 }326327 // 3. Emit fresh328 emit(fresh)329 }330}331```332333### Clean Architecture Module Layers334```335shared/src/commonMain/kotlin/com/app/336├── domain/ # Pure Kotlin, no framework dependencies337│ ├── model/ # Domain entities (data classes)338│ ├── repository/ # Repository interfaces339│ └── usecase/ # Business logic (single responsibility)340├── data/ # Implements domain interfaces341│ ├── remote/ # Ktor API clients342│ ├── local/ # SQLDelight DAOs343│ └── repository/ # Repository implementations344├── di/ # Koin module definitions345└── platform/ # expect declarations346 └── Platform.kt347```348349Domain layer has zero dependencies on Ktor, SQLDelight, or Compose. This makes it testable in `commonTest` without platform setup. Repository interfaces are in domain (`OrderRepository`), implementations are in data (`OrderRepositoryImpl`).350351### Sealed Class State Management352```kotlin353sealed interface UiState<out T> {354 data object Loading : UiState<Nothing>355 data class Success<T>(val data: T) : UiState<T>356 data class Error(val message: String, val throwable: Throwable?) : UiState<Nothing>357}358359@Composable360fun <T> ContentView(361 state: UiState<T>,362 onRetry: () -> Unit,363 content: @Composable (T) -> Unit364) {365 when (state) {366 is UiState.Loading -> ShimmerLoading()367 is UiState.Success -> content(state.data)368 is UiState.Error -> ErrorView(state.message, onRetry)369 }370}371```372373## Anti-Patterns (Expanded)374375- **Sharing platform-specific types**: `java.io.File` in commonMain breaks compilation. Use expect/actual or interfaces for platform types.376- **One-shot expect/actual for everything**: Each `expect` declaration needs `actual` on every platform — increases maintenance. Prefer interfaces with platform DI.377- **Blocking main thread on iOS**: Kotlin coroutines on iOS may dispatch to the main thread. Use `Dispatchers.Main.immediate` with `withContext` for UI updates. Never use `runBlocking` on main thread in iOS.378- **No iOS memory optimization**: Kotlin/Native frameworks shipped with debug symbols by default. Strip with `isStatic = true` and set `embedAndSignAppleFrameworkForXcode` to use release builds in production.379- **`kotlin.test` vs platform test runners**: `commonTest` uses `kotlin.test` assertions, which differ from JUnit 5 and XCTest. Teams must learn KMP test patterns.380- **Over-reliance on Compose for everything**: Compose Multiplatform can't render native MapView, CameraPreview, or ARKit/ARCore. For 20% of features that need native components, use `expect` composable wrappers and keep them thin.381- **Mixing CocoaPods and SPM**: Choose one dependency manager for iOS. CocoaPods has better KMP integration. SPM support is improving but still has edge cases with transitive dependencies.382- **Missing `@ThreadLocal` on iOS objects**: iOS object initializers run on arbitrary threads. Use `@ThreadLocal` annotation or `AtomicReference` for mutable state accessed from multiple threads.383384## Configuration Reference385386```kotlin387// build.gradle.kts (shared module)388plugins {389 id("org.jetbrains.kotlin.multiplatform") version "2.0.21"390 id("org.jetbrains.kotlin.plugin.serialization") version "2.0.21"391 id("org.jetbrains.compose") version "1.7.1"392 id("app.cash.sqldelight") version "2.0.2"393}394kotlin {395 androidTarget { compilations.all { kotlinOptions { jvmTarget = "17" } } }396 listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {397 it.binaries.framework { baseName = "shared"; isStatic = true }398 }399 sourceSets {400 commonMain.dependencies {401 implementation("io.ktor:ktor-client-core:3.0.3")402 implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")403 implementation("app.cash.sqldelight:runtime:2.0.2")404 implementation(compose.runtime); implementation(compose.foundation)405 implementation(compose.material3); implementation(compose.ui)406 }407 androidMain.dependencies {408 implementation("io.ktor:ktor-client-okhttp:3.0.3")409 implementation("app.cash.sqldelight:android-driver:2.0.2")410 }411 iosMain.dependencies {412 implementation("io.ktor:ktor-client-darwin:3.0.3")413 implementation("app.cash.sqldelight:native-driver:2.0.2")414 }415 }416}417```418419## References420 - references/kmm-concurrency.md — KMM Concurrency — Coroutines, Flows, and Threading421 - references/kmm-networking.md — KMM Networking — Ktor, SQLDelight, Serialization422 - references/kmp-compose.md — Compose Multiplatform423 - references/kmp-structure.md — KMP Module Structure424 - references/kotlin-multiplatform-advanced.md — Kotlin Multiplatform Advanced Topics425 - references/kotlin-multiplatform-fundamentals.md — Kotlin Multiplatform Fundamentals426 - references/platform-specific.md — Platform-Specific Implementations427## Handoff428Hand off to platform-specific iOS or Android skills when expect/actual implementations need deep platform API knowledge.429## Implementation Patterns430431### Observer Pattern for Event Handling432`433interface EventObserver<T> {434 onEvent(event: T): Promise<void>;435}436437class EventBus<T> {438 private observers: Set<EventObserver<T>> = new Set();439 subscribe(observer: EventObserver<T>): void {440 this.observers.add(observer);441 }442 unsubscribe(observer: EventObserver<T>): void {443 this.observers.delete(observer);444 }445 async emit(event: T): Promise<void> {446 const results = Array.from(this.observers).map(o => o.onEvent(event));447 await Promise.allSettled(results);448 }449}450`451452### Configuration-Driven Approach453`454config:455 defaults:456 timeout: 30s457 retryCount: 3458 overrides:459 production:460 timeout: 60s461 retryCount: 5462 development:463 timeout: 300s464 retryCount: 1465`466467## Production Considerations468469### Deployment Checklist470- [ ] Configuration validated against schema before startup471- [ ] Health check endpoints registered and monitored472- [ ] Graceful shutdown with draining period (30s timeout)473- [ ] Resource limits configured (CPU, memory, file descriptors)474- [ ] Log level set appropriate for environment475- [ ] Metrics endpoint secured and exposed476- [ ] Rate limiting configured per-tier477- [ ] TLS certificates valid and auto-renewing478- [ ] Database migrations run as separate deployment step479- [ ] Feature flags ready for gradual rollout480481### Monitoring and Alerting482| Metric | Threshold | Severity | Action |483|--------|-----------|----------|--------|484| Error rate | > 1% over 5min | Critical | Page on-call |485| p99 latency | > 2s over 5min | Warning | Investigate |486| Throughput drop | > 50% over 1min | Critical | Check upstream |487| Queue depth | > 1000 over 1min | Warning | Scale consumers |488| Disk usage | > 85% | Warning | Clean or expand |489| Memory usage | > 90% heap | Critical | Restart or scale |490491## Anti-Patterns492493| Anti-Pattern | Symptom | Root Cause | Solution |494|-------------|---------|------------|----------|495| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |496| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |497| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |498| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |499500## Performance Optimization501502### Caching Strategy503Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge).504Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).505506### Resource Pooling507- Database connections: Pool of reusable connections (HikariCP, pgBouncer)508- HTTP connections: Keep-alive + connection pooling for external calls509- Thread pool: Bounded thread pools for async task execution510511### Profiling Methodology5121. Establish baseline with production traffic profile5132. Profile CPU with sampling profiler (pprof, perf, async-profiler)5143. Profile memory with heap dumps and allocation tracking5154. Profile I/O with strace/perf trace for syscall analysis5165. Profile latency with distributed tracing (OpenTelemetry)5176. Identify bottleneck, formulate hypothesis, implement fix5187. Re-profile to verify improvement, repeat519520## Security Considerations521522### Threat Modeling (STRIDE)523- Spoofing: Identity validation, authentication524- Tampering: Integrity checks, digital signatures525- Repudiation: Audit logs, non-repudiation526- Information disclosure: Encryption, access control527- Denial of service: Rate limiting, resource quotas528- Elevation of privilege: Principle of least privilege529530### Supply Chain Security531- Dependency scanning: Snyk, Dependabot, Trivy532- SBOM generation: CycloneDX or SPDX format533- Signed commits: GPG or SSH commit signing534- Artifact verification: Checksum validation, signature verification535536### Secrets Management537- Secrets never in code — always in secrets manager (Vault, AWS Secrets Manager)538- Rotation policy: Rotate database credentials every 90 days539- Access audit: Log every secrets access, alert on anomalies540- Encryption at rest and in transit for all secrets541- Principle of least privilege: each service gets only its own secrets542543## Rules544- Default-deny security posture — allow only explicitly required access.545- All inputs validated, all outputs encoded, all errors handled.546- Defend in depth — multiple layers of security controls.547- Fail securely — errors default to safe behavior.548- Log security-relevant events for audit and investigation.549- Keep dependencies updated — automate vulnerability scanning.550- Design for observability from day one, not as an afterthought.551- Document all architectural decisions with rationale.552- Review code for security, performance, and correctness before merging.