| name | kotlin-patterns |
| description | Idiomatic Kotlin patterns and best practices. Use when writing, reviewing, or refactoring Kotlin code involving coroutines, null safety, sealed classes, DSL builders, or extension functions. |
| origin | MCC |
Kotlin Development Patterns
Idiomatic Kotlin patterns and best practices for building robust, efficient, and maintainable applications.
When to Use
- Writing new Kotlin code
- Reviewing Kotlin code
- Refactoring existing Kotlin code
- Designing Kotlin modules or libraries
- Configuring Gradle Kotlin DSL builds
How It Works
This skill enforces idiomatic Kotlin conventions across seven key areas: null safety using the type system and safe-call operators, immutability via val and copy() on data classes, sealed classes and interfaces for exhaustive type hierarchies, structured concurrency with coroutines and Flow, extension functions for adding behaviour without inheritance, type-safe DSL builders using @DslMarker and lambda receivers, and Gradle Kotlin DSL for build configuration.
Examples
Null safety with Elvis operator:
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user?.email ?: "unknown@example.com"
}
Sealed class for exhaustive results:
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Failure(val error: AppError) : Result<Nothing>()
data object Loading : Result<Nothing>()
}
Structured concurrency with async/await:
suspend fun fetchUserWithPosts(userId: String): UserProfile =
coroutineScope {
val user = async { userService.getUser(userId) }
val posts = async { postService.getUserPosts(userId) }
UserProfile(user = user.await(), posts = posts.await())
}
Core Principles
1. Null Safety
fun getUser(id: String): User {
return userRepository.findById(id)
?: throw UserNotFoundException("User $id not found")
}
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user?.email ?: "unknown@example.com"
}
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user!!.email
}
2. Immutability by Default
data class User(val id: String, val name: String, val email: String)
fun updateEmail(user: User, newEmail: String): User = user.copy(email = newEmail)
val users: List<User> = listOf(user1, user2)
val filtered = users.filter { it.email.isNotBlank() }
3. Expression Bodies and Single-Expression Functions
fun isAdult(age: Int): Boolean = age >= 18
fun formatFullName(first: String, last: String): String =
"$first $last".trim()
fun statusMessage(code: Int): String = when (code) {
200 -> "OK"
404 -> "Not Found"
500 -> "Internal Server Error"
else -> "Unknown status: $code"
}
4. Data Classes and Value Classes
data class CreateUserRequest(
val name: String,
val email: String,
val role: Role = Role.USER,
)
@JvmInline
value class UserId(val value: String) {
init { require(value.isNotBlank()) { "UserId cannot be blank" } }
}
@JvmInline
value class Email(val value: String) {
init { require('@' in value) { "Invalid email: $value" } }
}
Scope Functions
val length: Int? = name?.let { it.trim().length }
val user = User().apply { name = "Alice"; email = "alice@example.com" }
val user = createUser(request).also { logger.info("Created user: ${it.id}") }
val result = connection.run { prepareStatement(sql); executeQuery() }
val csv = with(StringBuilder()) {
appendLine("name,email")
users.forEach { appendLine("${it.name},${it.email}") }
toString()
}
Anti-pattern: Avoid nesting scope functions. Chain safe calls instead:
val city = user?.address?.city
city?.let { println(it) }
Extension Functions
fun String.toSlug(): String =
lowercase()
.replace(Regex("[^a-z0-9\\s-]"), "")
.replace(Regex("\\s+"), "-")
.trim('-')
class UserService {
private fun User.isActive(): Boolean =
status == Status.ACTIVE && lastLogin.isAfter(Instant.now().minus(30, ChronoUnit.DAYS))
fun getActiveUsers(): List<User> = userRepository.findAll().filter { it.isActive() }
}
Error Handling Patterns
suspend fun createUser(request: CreateUserRequest): Result<User> = runCatching {
require(request.name.isNotBlank()) { "Name cannot be blank" }
require('@' in request.email) { "Invalid email format" }
val user = User(id = UserId(UUID.randomUUID().toString()), name = request.name, email = Email(request.email))
userRepository.save(user)
user
}
fun withdraw(account: Account, amount: Money): Account {
require(amount.value > 0) { "Amount must be positive: $amount" }
check(account.balance >= amount) { "Insufficient balance: ${account.balance} < $amount" }
return account.copy(balance = account.balance - amount)
}
Collection Operations
val activeAdminEmails: List<String> = users
.filter { it.role == Role.ADMIN && it.isActive }
.sortedBy { it.name }
.map { it.email }
val usersByRole: Map<Role, List<User>> = users.groupBy { it.role }
val usersById: Map<UserId, User> = users.associateBy { it.id }
val (active, inactive) = users.partition { it.isActive }
Quick Reference: Kotlin Idioms
| Idiom | Description |
|---|
val over var | Prefer immutable variables |
data class | For value objects with equals/hashCode/copy |
sealed class/interface | For restricted type hierarchies |
value class | For type-safe wrappers with zero overhead |
Expression when | Exhaustive pattern matching |
Safe call ?. | Null-safe member access |
Elvis ?: | Default value for nullables |
let/apply/also/run/with | Scope functions for clean code |
| Extension functions | Add behavior without inheritance |
copy() | Immutable updates on data classes |
require/check | Precondition assertions |
Coroutine async/await | Structured concurrent execution |
Flow | Cold reactive streams |
sequence | Lazy evaluation |
Delegation by | Reuse implementation without inheritance |
Anti-Patterns to Avoid
Remember: Kotlin code should be concise but readable. Leverage the type system for safety, prefer immutability, and use coroutines for concurrency. When in doubt, let the compiler help you.
Reference Files
- coroutines-and-flow.md — Structured concurrency (coroutineScope, supervisorScope), Flow operators, cancellation and cleanup patterns
- sealed-and-dsl.md — Sealed class/interface hierarchies, type-safe DSL builders, property/interface delegation, sequences, and Gradle Kotlin DSL configuration