How to Use Kotlin sortedBy for Multi-Field Sorting with Mixed Ascending and Descending Order

Use sortedWith combined with compareBy and chained thenBy or thenByDescending operators to sort Kotlin collections by multiple fields with mixed sort directions.

While kotlin sortedBy provides a concise way to sort by a single field, sorting by multiple criteria requires composing comparators. The Kotlin standard library implements this through a hierarchy of comparator-building functions that delegate to stable sorting algorithms under the hood.

Why Kotlin sortedBy Cannot Sort by Multiple Fields Alone

The sortedBy extension function is designed for single-field sorting. According to the implementation in libraries/stdlib/common/src/generated/_Collections.kt (lines 1067-1080), sortedBy delegates directly to sortedWith(compareBy(selector)) and returns a new List, not a comparator:

// From _Collections.kt lines 1067-1080
public inline fun <T, R : Comparable<R>> Iterable<T>.sortedBy(crossinline selector: (T) -> R?): List<T> {
    return sortedWith(compareBy(selector))
}

public inline fun <T, R : Comparable<R>> Iterable<T>.sortedByDescending(crossinline selector: (T) -> R?): List<T> {
    return sortedWith(compareByDescending(selector))
}

Because sortedBy materializes a list immediately, you cannot chain additional sorting criteria to it. To evaluate multiple fields, you must build a composite comparator first, then pass it to sortedWith.

Using compareBy and thenBy with sortedWith for Multi-Field Sorting

The comparator composition API resides in libraries/stdlib/src/kotlin/comparisons/Comparisons.kt. Lines 89-120 define compareBy and compareByDescending, while lines 142-172 implement thenBy and thenByDescending as extension functions on Comparator.

Sorting by Primary and Secondary Keys

To sort by multiple fields, start with compareBy for the primary key, then chain thenBy for additional ascending criteria:

data class Person(val name: String, val age: Int, val height: Double)

val people = listOf(
    Person("Alice", 30, 165.0),
    Person("Bob", 25, 180.0),
    Person("Alice", 22, 170.0)
)

// Primary: name (ascending), Secondary: age (ascending)
val sorted = people.sortedWith(
    compareBy<Person> { it.name }
        .thenBy { it.age }
)

As implemented in Comparisons.kt, thenBy returns a new comparator that first evaluates the original comparator, and only if the result is zero (equal) does it evaluate the subsequent selector.

Mixing Ascending and Descending Order

To mix sort directions, use compareByDescending for the primary field or thenByDescending for subsequent fields:

// Primary: name (ascending), Secondary: age (descending), Tertiary: height (ascending)
val mixedSort = people.sortedWith(
    compareBy<Person> { it.firstName }
        .thenByDescending { it.age }
        .thenBy { it.height }
)

According to Comparisons.kt lines 142-172, thenByDescending wraps the selector with compareByDescending logic, inverting the natural order of the comparable result.

Complete Code Examples for Kotlin Multi-Field Sorting

Sorting Data Classes by Multiple Properties

This example demonstrates the full implementation pattern using sortedWith with a composed comparator, referencing the actual stdlib delegation to Array.sortWith as found in _Collections.kt lines 97-104:

data class Employee(
    val department: String,
    val salary: Int,
    val name: String
)

val employees = listOf(
    Employee("Engineering", 90000, "Alice"),
    Employee("Sales", 85000, "Bob"),
    Employee("Engineering", 120000, "Charlie"),
    Employee("Engineering", 90000, "David")
)

// Sort: department asc → salary desc → name asc
val result = employees.sortedWith(
    compareBy<Employee> { it.department }
        .thenByDescending { it.salary }
        .thenBy { it.name }
)

// Result:
// 1. Engineering, 120000, Charlie
// 2. Engineering, 90000, Alice
// 3. Engineering, 90000, David
// 4. Sales, 85000, Bob

The sortedWith function delegates to a stable sort algorithm (TimSort in the JVM implementation), ensuring that when salary values are equal, the original relative order of Alice and David is preserved, and then name is evaluated to break the tie.

Sorting Map Entries by Key and Value

You can apply the same comparator composition to Map entries, as they are treated as Iterable objects:

val scores = mapOf(
    "Team A" to 85,
    "Team C" to 92,
    "Team B" to 85,
    "Team A" to 90
)

// Sort by key ascending, then by value descending
val sorted = scores.entries.sortedWith(
    compareBy<Map.Entry<String, Int>> { it.key }
        .thenByDescending { it.value }
)
    .associate { it.key to it.value }

// Result: {Team A=90, Team B=85, Team C=92}
// Note: Map keys must be unique, so duplicate "Team A" entries would overwrite;
// this example assumes handling of entry pairs before conversion back to Map

Summary

  • sortedBy handles single-field sorting by delegating to compareBy, but returns a List that cannot be chained for additional criteria.
  • sortedWith accepts a Comparator<T>, enabling multi-field sorting when combined with comparator composition functions.
  • compareBy creates the primary comparator; thenBy and thenByDescending chain secondary and tertiary criteria as defined in libraries/stdlib/src/kotlin/comparisons/Comparisons.kt.
  • The implementation in libraries/stdlib/common/src/generated/_Collections.kt guarantees stable sorting, preserving the relative order of elements considered equal by the comparator.

Frequently Asked Questions

Can I chain multiple sortedBy calls to sort by multiple fields?

No. Each sortedBy call returns a new List sorted by a single selector, and subsequent calls re-sort the entire list from scratch, destroying the previous ordering. To sort by multiple fields, use sortedWith with a composed comparator built from compareBy and thenBy chains.

Is Kotlin's multi-field sorting stable?

Yes. According to the implementation in libraries/stdlib/common/src/generated/_Collections.kt, both sortedBy and sortedWith delegate to a stable sorting algorithm (TimSort on the JVM). This means that when two elements are considered equal by your comparator, their original relative order in the source collection is preserved.

How do I handle null values when sorting by multiple fields?

Use the null-safe variants compareByDescending with nullable selectors, or wrap selectors with nullsFirst() and nullsLast() from the standard library. For example: compareBy<Person>(nullsFirst(), { it.middleName }).thenBy { it.lastName }. The Comparisons.kt file provides these null-handling comparator extensions.

What is the performance difference between sortedBy and sortedWith?

There is no significant performance difference in the sorting itself—both use the same underlying stable sort algorithm. However, sortedBy creates a temporary comparator internally via compareBy(selector), while sortedWith allows you to reuse a composed comparator across multiple collections. For multi-field sorting, sortedWith with a pre-built comparator is actually more efficient than chaining multiple sortedBy calls, which would create intermediate lists and re-sort data repeatedly.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →