Skip to content
Back to skills

Kotlin Patterns

ASecurity

Kotlin: coroutines, Flow, sealed/data classes, null safety, Ktor, Compose, KMP. Triggers: Kotlin, coroutine, Flow, suspend, Ktor, Jetpack Compose, KMP, kotlinx.

  • 177 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 27, 2026
developmentgokotlinsqlspringtestingapiperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add softspark/ai-toolkit --skill kotlin-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Kotlin Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Kotlin Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/softspark-kotlin-patterns/badge)](https://www.skillsdirectory.com/skills/softspark-kotlin-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: kotlin-patterns
description: "Kotlin: coroutines, Flow, sealed/data classes, null safety, Ktor, Compose, KMP. Triggers: Kotlin, coroutine, Flow, suspend, Ktor, Jetpack Compose, KMP, kotlinx."
effort: medium
user-invocable: false
allowed-tools: Read
---

# Kotlin Patterns Skill

## Project Structure

### Gradle KTS Multi-Module Layout
```
project-root/
├── build.gradle.kts
├── settings.gradle.kts
├── gradle/libs.versions.toml
├── app/
│   ├── build.gradle.kts
│   └── src/{main,test}/kotlin/com/example/app/
├── domain/
│   └── src/main/kotlin/com/example/domain/
│       ├── model/
│       ├── repository/
│       └── usecase/
└── infrastructure/
    └── src/main/kotlin/com/example/infra/
```

### settings.gradle.kts
```kotlin
rootProject.name = "my-project"
dependencyResolutionManagement {
    versionCatalogs { create("libs") { from(files("gradle/libs.versions.toml")) } }
}
include(":app", ":domain", ":infrastructure")
```

### Module build.gradle.kts
```kotlin
plugins {
    alias(libs.plugins.kotlin.jvm)
    alias(libs.plugins.kotlin.serialization)
}
dependencies {
    implementation(project(":domain"))
    implementation(libs.kotlinx.coroutines.core)
    testImplementation(libs.bundles.testing)
}
kotlin { jvmToolchain(21) }
```

---

## Idioms / Code Style

### Data Classes + Value Classes
```kotlin
data class User(
    val id: UserId,
    val name: String,
    val email: String,
    val role: Role = Role.USER,
) {
    init {
        require(name.isNotBlank()) { "Name must not be blank" }
        require(email.contains("@")) { "Invalid email format" }
    }
}

@JvmInline
value class UserId(val value: String)  // Zero-overhead type-safe ID
enum class Role { ADMIN, USER, GUEST }
```

### Sealed Interfaces for Domain Modeling
```kotlin
sealed interface PaymentResult {
    data class Success(val transactionId: String, val amount: Money) : PaymentResult
    data class Declined(val reason: String) : PaymentResult
    data class Error(val exception: Throwable) : PaymentResult
}

// Exhaustive when -- compiler enforces all branches
fun handlePayment(result: PaymentResult): String = when (result) {
    is PaymentResult.Success -> "Paid: ${result.amount}"
    is PaymentResult.Declined -> "Declined: ${result.reason}"
    is PaymentResult.Error -> "Error: ${result.exception.message}"
}
```

### Extension Functions
```kotlin
fun String.toSlug(): String =
    lowercase().replace(Regex("[^a-z0-9\\s-]"), "").replace(Regex("\\s+"), "-").trim('-')

// Scoped extensions -- visible only inside containing class
class OrderService {
    private fun Order.totalWithTax(): Money = total * (1 + taxRate)
}
```

### Null Safety
```kotlin
fun getDisplayName(user: User?): String =
    user?.name?.takeIf { it.isNotBlank() } ?: "Anonymous"

fun processEmail(email: String?) { email?.let { sendWelcomeEmail(it) } }

fun loadConfig(path: String?): Config {
    val resolved = requireNotNull(path) { "Config path must not be null" }
    return parseConfig(resolved)
}
```

### Scope Functions

| Function | Ref | Returns | Use case |
|----------|-----|---------|----------|
| `let` | `it` | Lambda result | Null check + transform |
| `run` | `this` | Lambda result | Config + compute |
| `apply` | `this` | Object itself | Object initialization |
| `also` | `it` | Object itself | Side effects |

```kotlin
val conn = Connection().apply { host = "localhost"; port = 5432 }
fun createUser(req: CreateUserRequest): User =
    userRepository.save(req.toUser()).also { logger.info("Created: ${it.id}") }
```

### Type-Safe Builders (DSL)
```kotlin
fun html(block: HtmlBuilder.() -> Unit): String = HtmlBuilder().apply(block).build()

val page = html {
    head { title("My Page") }
    body { p("Hello, world!") }
}
```

---

## Error Handling

### Result<T> and runCatching
```kotlin
fun findUser(id: UserId): Result<User> = runCatching {
    userRepository.findById(id) ?: throw UserNotFoundException(id)
}

fun getUserDisplayName(id: UserId): String =
    findUser(id).map { it.name }.recover { "Unknown User" }.getOrThrow()

fun handleLookup(id: UserId): Response = findUser(id).fold(
    onSuccess = { Response.ok(it) },
    onFailure = { Response.notFound(it.message) },
)
```

### Sealed Class Error Hierarchy
```kotlin
sealed class DomainError(override val message: String) : Exception(message) {
    data class NotFound(val resource: String, val id: String) : DomainError("$resource not found: $id")
    data class Validation(val field: String, val reason: String) : DomainError("Invalid $field: $reason")
    data class Conflict(val detail: String) : DomainError("Conflict: $detail")
}

fun handleError(error: DomainError): Response = when (error) {
    is DomainError.NotFound -> Response.status(404).body(error.message)
    is DomainError.Validation -> Response.status(422).body(error.message)
    is DomainError.Conflict -> Response.status(409).body(error.message)
}
```

### Preconditions
```kotlin
fun transferMoney(from: Account, to: Account, amount: Money) {
    require(amount.value > 0) { "Transfer amount must be positive" }
    require(from.id != to.id) { "Cannot transfer to same account" }
    check(from.balance >= amount) { "Insufficient funds: ${from.balance}" }
}
```

---

## Testing Patterns

### JUnit 5 + MockK
```kotlin
class UserServiceTest {
    private val repository = mockk<UserRepository>()
    private val notifier = mockk<NotificationService>(relaxed = true)
    private val service = UserService(repository, notifier)

    @Test
    fun `creates user and sends welcome notification`() {
        val expected = User(UserId("1"), "Alice", "alice@test.com")
        every { repository.save(any()) } returns expected

        val result = service.createUser(CreateUserRequest("Alice", "alice@test.com"))

        assertThat(result).isEqualTo(expected)
        verify(exactly = 1) { notifier.sendWelcome(expected) }
    }

    @Test
    fun `throws on duplicate email`() {
        every { repository.save(any()) } throws DomainError.Conflict("Email exists")
        assertThrows<DomainError.Conflict> {
            service.createUser(CreateUserRequest("Bob", "dup@test.com"))
        }
    }
}
```

### Kotest + Property-Based Testing
```kotlin
class MoneySpec : FunSpec({
    test("addition is commutative") {
        checkAll(Arb.positiveLong(), Arb.positiveLong()) { a, b ->
            Money(a) + Money(b) shouldBe Money(b) + Money(a)
        }
    }
    test("cannot create negative money") {
        shouldThrow<IllegalArgumentException> { Money(-1) }
    }
})
```

### assertSoftly + Parameterized Tests
```kotlin
@Test
fun `user has correct defaults`() {
    val user = User.create("Alice", "alice@test.com")
    assertSoftly(user) {
        name shouldBe "Alice"
        role shouldBe Role.USER
        isActive shouldBe true
    }
}

@ParameterizedTest
@CsvSource("alice@test.com, true", "not-an-email, false", "'', false")
fun `validates email format`(input: String, expected: Boolean) {
    assertThat(isValidEmail(input)).isEqualTo(expected)
}
```

---

## Common Frameworks

### Spring Boot with Kotlin
```kotlin
@RestController
@RequestMapping("/api/v1/users")
class UserController(private val userService: UserService) {
    @GetMapping("/{id}")
    fun getUser(@PathVariable id: String): ResponseEntity<UserDto> =
        userService.findById(UserId(id))?.let { ResponseEntity.ok(it.toDto()) }
            ?: ResponseEntity.notFound().build()

    @PostMapping
    fun createUser(@Valid @RequestBody req: CreateUserRequest): ResponseEntity<UserDto> {
        val user = userService.create(req)
        return ResponseEntity.created(URI("/api/v1/users/${user.id.value}")).body(user.toDto())
    }
}
```

### Ktor
```kotlin
fun Application.configureRouting() {
    routing {
        route("/api/v1/users") {
            get { call.respond(userService.listAll()) }
            get("/{id}") {
                val id = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest)
                val user = userService.findById(UserId(id)) ?: return@get call.respond(HttpStatusCode.NotFound)
                call.respond(user)
            }
            post {
                val req = call.receive<CreateUserRequest>()
                call.respond(HttpStatusCode.Created, userService.create(req))
            }
        }
    }
}

fun Application.configurePlugins() {
    install(ContentNegotiation) { json() }
    install(StatusPages) {
        exception<DomainError.NotFound> { call, e -> call.respond(HttpStatusCode.NotFound, e.message) }
    }
}
```

### Exposed (SQL DSL)
```kotlin
object Users : Table("users") {
    val id = varchar("id", 36)
    val name = varchar("name", 255)
    val email = varchar("email", 255).uniqueIndex()
    val role = enumerationByName<Role>("role", 20)
    override val primaryKey = PrimaryKey(id)
}

suspend fun findByRole(role: Role): List<User> = newSuspendedTransaction(Dispatchers.IO) {
    Users.selectAll().where { Users.role eq role }.map { it.toUser() }
}
```

### kotlinx.serialization
```kotlin
@Serializable
data class ApiResponse<T>(val data: T, val meta: Meta? = null)

@Serializable
data class Meta(val page: Int, val totalPages: Int, val totalItems: Long)
```

### Koin (DI)
```kotlin
val appModule = module {
    singleOf(::UserRepository)
    singleOf(::UserService)
    factoryOf(::CreateUserUseCase)
}
fun Application.configureKoin() { install(Koin) { modules(appModule) } }
```

---

## Performance Tips

### Inline Functions
```kotlin
inline fun <T> measureTimeAndReturn(block: () -> T): Pair<T, Duration> {
    val start = System.nanoTime()
    val result = block()
    return result to Duration.ofNanos(System.nanoTime() - start)
}
// crossinline: prevents non-local returns
inline fun transaction(crossinline block: () -> Unit) {
    begin(); try { block() } catch (e: Exception) { rollback(); throw e }; commit()
}
```

### Value Classes
```kotlin
@JvmInline value class Email(val value: String) {
    init { require(value.contains("@")) }
}
@JvmInline value class Meters(val value: Double)
// Compiles to raw String/Double -- zero object allocation
```

### Sequences vs Lists
```kotlin
// Bad: 3 intermediate lists
users.filter { it.isActive }.map { it.name }.take(10)
// Good: lazy, single pass, stops after 10
users.asSequence().filter { it.isActive }.map { it.name }.take(10).toList()
// Rule: sequences for 3+ chained ops on 1000+ elements
```

### Coroutines
```kotlin
suspend fun loadDashboard(userId: String): Dashboard = coroutineScope {
    val profile = async { userService.getProfile(userId) }
    val orders = async { orderService.getRecent(userId) }
    val notifs = async { notificationService.getUnread(userId) }
    Dashboard(profile.await(), orders.await(), notifs.await())
}

fun observeOrders(): Flow<Order> =
    orderRepository.observe().map { it.toDomain() }.catch { emit(Order.EMPTY) }.flowOn(Dispatchers.IO)
```

### Avoid Reflection
```kotlin
// Bad: Gson uses reflection -- slow, no compile-time safety
val json = Gson().toJson(user)
// Good: kotlinx.serialization uses compile-time codegen
@Serializable data class User(val id: String, val name: String)
val json = Json.encodeToString(user)
```

---

## Build / Package Management

### Version Catalog (gradle/libs.versions.toml)
```toml
[versions]
kotlin = "2.1.0"
coroutines = "1.10.1"
ktor = "3.0.3"
kotest = "5.9.1"
mockk = "1.13.14"

[libraries]
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.7.3" }
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
kotest-runner = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }

[bundles]
testing = ["mockk", "kotest-runner", "kotest-assertions"]

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
```

### Kotlin Multiplatform
```kotlin
kotlin {
    jvm(); iosArm64(); iosSimulatorArm64(); js(IR) { browser() }
    sourceSets {
        commonMain.dependencies {
            implementation(libs.kotlinx.coroutines.core)
            implementation(libs.kotlinx.serialization.json)
        }
        commonTest.dependencies { implementation(kotlin("test")) }
        jvmMain.dependencies { implementation(libs.ktor.server.core) }
    }
}
```

---

## Anti-Patterns to Avoid

| Anti-Pattern | Problem | Solution |
|--------------|---------|----------|
| `!!` everywhere | NPE at runtime | Safe calls, elvis, `requireNotNull` |
| `var` by default | Mutability bugs | Default to `val` |
| Catching `Exception` | Swallows `CancellationException` | Catch specific types |
| Mutable data class props | Breaks hashCode/equals | Use `val` in data classes |
| Stringly-typed IDs | Mix up userId/orderId | Value classes |
| Blocking in coroutine | Thread starvation | `withContext(Dispatchers.IO)` |
| Ignoring `Result` failures | Silent errors | Always handle both paths |
| God object / util class | No cohesion | Extension functions |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.