Idioms

See also: https://kotlinlang.org/docs/idioms.html

Utilize Kotlin's built-in features - write idiomatic code. This will make your code much simpler (especially simpler to read), and be less lines overall (thus faster to write).

Some examples:

Use Kotlin Null Safety features

// Good ✅
val length = str?.length ?: 0
val userCity = user?.address?.city?.uppercase() ?: "UNKNOWN"

data class Report(
    val user: User?,
    val statistics: Statistics?
) {
    fun getFormattedStats() = statistics?.let { stats ->
        "${stats.visits} visits, ${stats.conversion}% conversion"
    } ?: "No statistics available"
}
// Avoid ❌ - Java-style null checking hell
val length = if (str != null) str.length else 0

String userCity;
if (user != null) {
    Address address = user.getAddress();
    if (address != null) {
        String city = address.getCity();
        if (city != null) {
            userCity = city.toUpperCase();
        } else {
            userCity = "UNKNOWN";
        }
    } else {
        userCity = "UNKNOWN";
    }
} else {
    userCity = "UNKNOWN";
}

Named Arguments

Some things just require a lot of data. Do not apply the Java way of inputting it into a function or class constructor:

// Avoid ❌ - Hard to read, prone to errors
createUserProfile("12345", "John", "Doe", "john@example.com", 
    "+1234567890", LocalDate.of(1990, 1, 1), 
    Address("123 Main St", "New York", "USA", "10001"),
    UserPreferences(true, false, true), UserRole.ADMIN)

Not only do we not know what exactly we input where, if the function / class constructor definition changes, this snippet above will be a serious liability.

// Good: Easy to read, definition changes are not dramatic ✅
createUserProfile(
    userId = "12345",
    firstName = "John",
    lastName = "Doe",
    email = "john@example.com",
    phoneNumber = "+1234567890",
    dateOfBirth = LocalDate.of(1990, 1, 1),
    address = Address(
        street = "123 Main St",
        city = "New York",
        country = "USA",
        postalCode = "10001"
    ),
    preferences = UserPreferences(
        newsletterEnabled = true,
        darkModeEnabled = false,
        notificationsEnabled = true
    ),
    role = UserRole.ADMIN
)

Use data classes

// Good ✅
data class User(
    val id: Long,
    val name: String,
    val email: String,
    val age: Int,
    val address: Address
)
// Avoid ❌ - Extremely verbose
class User {
    private val id: Long
    private val name: String
    private val email: String
    private val age: Int
    private val address: Address

    constructor(id: Long, name: String, email: String, age: Int, address: Address) {
        this.id = id
        this.name = name
        this.email = email
        this.age = age
        this.address = address
    }

    fun getId() = id
    fun getName() = name
    fun getEmail() = email
    fun getAge() = age
    fun getAddress() = address

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is User) return false
        return id == other.id &&
               name == other.name &&
               email == other.email &&
               age == other.age &&
               address == other.address
    }

    override fun hashCode(): Int {
        var result = id.hashCode()
        result = 31 * result + name.hashCode()
        result = 31 * result + email.hashCode()
        result = 31 * result + age
        result = 31 * result + address.hashCode()
        return result
    }

    override fun toString(): String {
        return "User(id=$id, name='$name', email='$email', age=$age, address=$address)"
    }
}

Do not split into Java-style utils classes

Of course, you should split your code over multiple functions to avoid having 1000 line long functions, and especially regarding DRY (don't repeat yourself) reuse reusable code.

// Good ✅ - Clean, chainable extension functions
fun List<Transaction>.totalAmount() = sumOf { it.amount }
fun List<Transaction>.forPeriod(start: LocalDate, end: LocalDate) = 
    filter { it.date in start..end }
fun List<Transaction>.byCategory(category: Category) = 
    filter { it.category == category }
fun List<Transaction>.groupByMonth() = 
    groupBy { it.date.yearMonth }
    .mapValues { it.value.totalAmount() }
fun Double.formatAsCurrency() = 
    "€%.2f".format(this)


// Usage looks like natural language! ✅
val result = transactions
    .forPeriod(startDate, endDate)
    .byCategory(Category.FOOD)
    .totalAmount()
    .formatAsCurrency()

val monthlyReport = transactions
    .forPeriod(startDate, endDate)
    .groupByMonth()
    .forEach { (month, amount) ->
        println("$month: ${amount.formatAsCurrency()}")
    }
// Avoid ❌ - Traditional utility class approach
class TransactionAnalyzer {
    fun calculateTotalAmount(transactions: List<Transaction>): Double {
        return transactions.sumOf { it.amount }
    }

    fun filterTransactionsByPeriod(
        transactions: List<Transaction>, 
        start: LocalDate, 
        end: LocalDate
    ): List<Transaction> {
        return transactions.filter { 
            it.date >= start && it.date <= end 
        }
    }

    fun filterTransactionsByCategory(
        transactions: List<Transaction>, 
        category: Category
    ): List<Transaction> {
        return transactions.filter { 
            it.category == category 
        }
    }

    fun groupTransactionsByMonth(
        transactions: List<Transaction>
    ): Map<YearMonth, Double> {
        return transactions
            .groupBy { YearMonth.from(it.date) }
            .mapValues { calculateTotalAmount(it.value) }
    }
}

class CurrencyFormatter {
    fun formatAmount(amount: Double): String {
        return String.format("€%.2f", amount)
    }
}

// Usage is verbose and harder to read ❌
val analyzer = TransactionAnalyzer()
val formatter = CurrencyFormatter()

val filteredByPeriod = analyzer.filterTransactionsByPeriod(
    transactions, 
    startDate, 
    endDate
)
val filteredByCategory = analyzer.filterTransactionsByCategory(
    filteredByPeriod, 
    Category.FOOD
)
val total = analyzer.calculateTotalAmount(filteredByCategory)
val result = formatter.formatAmount(total)

val monthlyTransactions = analyzer.filterTransactionsByPeriod(
    transactions, 
    startDate, 
    endDate
)
val monthlyTotals = analyzer.groupTransactionsByMonth(monthlyTransactions)
monthlyTotals.forEach { (month, amount) ->
    println("$month: ${formatter.formatAmount(amount)}")
}

Another example with date manipulation:

// Good ✅ - Expressive extension functions
fun LocalDate.isBusinessDay() = !isWeekend() && !isHoliday()
fun LocalDate.isWeekend() = dayOfWeek in listOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)
fun LocalDate.isHoliday() = this in holidayList

fun LocalDate.nextBusinessDay(): LocalDate = 
    plusDays(1).let { if (it.isBusinessDay()) it else it.nextBusinessDay() }

fun LocalDate.previousBusinessDay(): LocalDate = 
    minusDays(1).let { if (it.isBusinessDay()) it else it.previousBusinessDay() }

fun LocalDate.businessDaysUntil(end: LocalDate): Int = 
    if (this > end) 0
    else (this..end).count { it.isBusinessDay() }

// Usage is super clean! ✅
if (date.isBusinessDay()) {
    processPayment()
}

val nextWorkday = date.nextBusinessDay()
val workingDays = startDate.businessDaysUntil(endDate)
// Avoid ❌ - Traditional utility class approach
class DateUtils {
    fun isBusinessDay(date: LocalDate): Boolean {
        return !isWeekend(date) && !isHoliday(date)
    }

    private fun isWeekend(date: LocalDate): Boolean {
        return date.dayOfWeek == DayOfWeek.SATURDAY || 
               date.dayOfWeek == DayOfWeek.SUNDAY
    }

    private fun isHoliday(date: LocalDate): Boolean {
        return holidayList.contains(date)
    }

    fun findNextBusinessDay(date: LocalDate): LocalDate {
        var nextDate = date.plusDays(1)
        while (!isBusinessDay(nextDate)) {
            nextDate = nextDate.plusDays(1)
        }
        return nextDate
    }

    fun calculateBusinessDaysBetween(
        startDate: LocalDate, 
        endDate: LocalDate
    ): Int {
        if (startDate > endDate) return 0
        
        var count = 0
        var currentDate = startDate
        while (currentDate <= endDate) {
            if (isBusinessDay(currentDate)) {
                count++
            }
            currentDate = currentDate.plusDays(1)
        }
        return count
    }
}

Use collection operations instead of imperative style processing

// Good ✅
val adults = users
    .filter { it.age >= 18 }
    .map { it.name }

// Avoid ❌ - Imperative style
val adults = mutableListOf<String>()
for (user in users) {
    if (user.age >= 18) {
        adults.add(user.name)
    }
}

Longer example:

// Good ✅
val topActiveUsers = users
    .filter { it.isActive }
    .sortedByDescending { it.lastLoginDate }
    .take(10)
    .map { UserDTO(it.id, it.name, it.email) }

val userStatistics = users
    .groupBy { it.country }
    .mapValues { (_, users) ->
        users.fold(UserStats()) { stats, user ->
            stats.copy(
                totalUsers = stats.totalUsers + 1,
                activeUsers = stats.activeUsers + if (user.isActive) 1 else 0,
                averageAge = users.map { it.age }.average(),
                totalPurchases = stats.totalPurchases + user.purchases.size
            )
        }
    }
// Avoid ❌ - Imperative style
val topActiveUsers = mutableListOf<UserDTO>()
val activeUsers = mutableListOf<User>()
for (user in users) {
    if (user.isActive) {
        activeUsers.add(user)
    }
}
activeUsers.sortWith(compareByDescending { it.lastLoginDate })
var count = 0
for (user in activeUsers) {
    if (count < 10) {
        topActiveUsers.add(UserDTO(user.id, user.name, user.email))
        count++
    } else {
        break
    }
}

val userStatistics = mutableMapOf<String, UserStats>()
for (user in users) {
    val stats = userStatistics.getOrPut(user.country) { UserStats() }
    stats.totalUsers++
    if (user.isActive) {
        stats.activeUsers++
    }
    var totalAge = 0
    var userCount = 0
    for (u in users.filter { it.country == user.country }) {
        totalAge += u.age
        userCount++
    }
    stats.averageAge = totalAge.toDouble() / userCount
    stats.totalPurchases += user.purchases.size
}

Use When Expression Instead of (multiple) If-Else

// Good ✅
val result = when (value) {
    1 -> "One"
    2 -> "Two"
    else -> "Other"
}

// Avoid ❌ - Hard to read
val result = if (value == 1) {
    "One"
} else if (value == 2) {
    "Two"
} else {
    "Other"
}

Do not create multiple unnecessary overloads, use default arguments instead

// Good ✅
fun createUser(
    name: String,
    email: String,
    isActive: Boolean = true
)

// Avoid ❌ - Multiple overloads
fun createUser(name: String, email: String)
fun createUser(name: String, email: String, isActive: Boolean)

Use scope functions

// Good ✅ - Clean, nested scope functions with clear intent

class OrderProcessor {
    fun processOrder(rawOrder: RawOrder) = Order().apply {
        id = generateOrderId()
        timestamp = LocalDateTime.now()
        status = OrderStatus.PENDING
        
        customer = Customer().apply {
            rawOrder.customerData?.let { customerData ->
                id = customerData.id
                email = customerData.email.lowercase()
                name = customerData.name.trim()
                
                address = Address().apply {
                    customerData.address?.let { addressData ->
                        street = addressData.street.trim()
                        city = addressData.city.trim()
                        country = addressData.country.uppercase()
                        postalCode = addressData.postalCode.formatPostalCode()
                        
                        validateAddress()?.let { validatedAddress ->
                            longitude = validatedAddress.longitude
                            latitude = validatedAddress.latitude
                            formattedAddress = validatedAddress.formatted
                        }
                    }
                }
                
                preferences = CustomerPreferences().apply {
                    newsletterOptIn = customerData.marketingConsent
                    preferredLanguage = customerData.language ?: defaultLanguage
                    communicationChannel = customerData.preferredChannel
                    specialInstructions = customerData.notes?.trim()
                }
            }
        }
        
        items = rawOrder.items.map { rawItem ->
            OrderItem().apply {
                productId = rawItem.id
                quantity = rawItem.quantity.coerceAtLeast(1)
                price = priceService.getPrice(rawItem.id)
                
                customization = ItemCustomization().apply {
                    rawItem.customization?.let { custom ->
                        color = custom.color
                        size = custom.size
                        engraving = custom.text?.trim()
                        giftWrap = custom.isGift
                        
                        if (giftWrap) {
                            giftMessage = custom.giftMessage?.trim()
                            giftWrapStyle = custom.wrapStyle ?: defaultWrapStyle
                        }
                    }
                }
                
                subtotal = price * quantity
                tax = taxCalculator.calculateTax(subtotal, customer.address.country)
                total = subtotal + tax
            }
        }
        
        subtotal = items.sumOf { it.subtotal }
        tax = items.sumOf { it.tax }
        shippingCost = calculateShipping(items, customer.address)
        total = subtotal + tax + shippingCost
        
        discount = DiscountCalculator().run {
            calculateDiscount(
                subtotal = subtotal,
                customer = customer,
                items = items
            )
        }
        
        finalTotal = (total - discount).coerceAtLeast(0.0)
    }.also { order ->
        logger.info("Created order: ${order.id}")
        eventPublisher.publish(OrderCreatedEvent(order))
        notificationService.notifyCustomer(order)
        analyticsService.trackOrder(order)
    }
}
// Avoid ❌ - Nested null checks and tons of repetitive code
class OrderProcessor {
    fun processOrder(rawOrder: RawOrder): Order {
        val order = Order()
        order.id = generateOrderId()
        order.timestamp = LocalDateTime.now()
        order.status = OrderStatus.PENDING
        
        if (rawOrder.customerData != null) {
            val customer = Customer()
            customer.id = rawOrder.customerData.id
            customer.email = rawOrder.customerData.email.lowercase()
            customer.name = rawOrder.customerData.name.trim()
            
            if (rawOrder.customerData.address != null) {
                val address = Address()
                address.street = rawOrder.customerData.address.street.trim()
                address.city = rawOrder.customerData.address.city.trim()
                address.country = rawOrder.customerData.address.country.uppercase()
                address.postalCode = rawOrder.customerData.address.postalCode.formatPostalCode()
                
                val validatedAddress = validateAddress(address)
                if (validatedAddress != null) {
                    address.longitude = validatedAddress.longitude
                    address.latitude = validatedAddress.latitude
                    address.formattedAddress = validatedAddress.formatted
                }
                customer.address = address
            }
            
            val preferences = CustomerPreferences()
            preferences.newsletterOptIn = rawOrder.customerData.marketingConsent
            preferences.preferredLanguage = 
                if (rawOrder.customerData.language != null) 
                    rawOrder.customerData.language 
                else defaultLanguage
            preferences.communicationChannel = rawOrder.customerData.preferredChannel
            if (rawOrder.customerData.notes != null) {
                preferences.specialInstructions = rawOrder.customerData.notes.trim()
            }
            customer.preferences = preferences
            
            order.customer = customer
        }
        
        val orderItems = mutableListOf<OrderItem>()
        for (rawItem in rawOrder.items) {
            val item = OrderItem()
            item.productId = rawItem.id
            item.quantity = 
                if (rawItem.quantity < 1) 1 
                else rawItem.quantity
            item.price = priceService.getPrice(rawItem.id)
            
            if (rawItem.customization != null) {
                val customization = ItemCustomization()
                customization.color = rawItem.customization.color
                customization.size = rawItem.customization.size
                if (rawItem.customization.text != null) {
                    customization.engraving = rawItem.customization.text.trim()
                }
                customization.giftWrap = rawItem.customization.isGift
                
                if (customization.giftWrap) {
                    if (rawItem.customization.giftMessage != null) {
                        customization.giftMessage = rawItem.customization.giftMessage.trim()
                    }
                    customization.giftWrapStyle = 
                        rawItem.customization.wrapStyle ?: defaultWrapStyle
                }
                
                item.customization = customization
            }
            
            item.subtotal = item.price * item.quantity
            item.tax = taxCalculator.calculateTax(item.subtotal, order.customer.address.country)
            item.total = item.subtotal + item.tax
            
            orderItems.add(item)
        }
        order.items = orderItems
        
        var subtotal = 0.0
        var tax = 0.0
        for (item in orderItems) {
            subtotal += item.subtotal
            tax += item.tax
        }
        order.subtotal = subtotal
        order.tax = tax
        order.shippingCost = calculateShipping(orderItems, order.customer.address)
        order.total = subtotal + tax + order.shippingCost
        
        val discountCalculator = DiscountCalculator()
        order.discount = discountCalculator.calculateDiscount(
            subtotal = subtotal,
            customer = order.customer,
            items = orderItems
        )
        
        order.finalTotal = order.total - order.discount
        if (order.finalTotal < 0) {
            order.finalTotal = 0.0
        }
        
        logger.info("Created order: ${order.id}")
        eventPublisher.publish(OrderCreatedEvent(order))
        notificationService.notifyCustomer(order)
        analyticsService.trackOrder(order)
        
        return order
    }
}