How do I use immutability and copy-on-write with Kotlin collections?

In Kotlin, read-only collection interfaces plus copy-on-write updates are a common way to model immutable-style data.

Kotlin has two main collection interface families:

Mutable Read-only
MutableList<T> List<T>
MutableSet<T> Set<T>
MutableMap<K, V> Map<K, V>

Read-only interfaces prevent mutation through that reference, while copy-on-write means you create a new collection when you need a changed version.

Use read-only collection types

Prefer exposing List, Set, and Map instead of mutable versions:

class UserGroup(
    val users: List<String>
)

val group = UserGroup(listOf("Alice", "Bob"))

// group.users.add("Charlie") // Does not compile

This prevents callers from mutating the collection directly.

Copy-on-write with lists

To “modify” a list, create a new one:

val users = listOf("Alice", "Bob")

val withCharlie = users + "Charlie"
val withoutAlice = users - "Alice"

println(users)        // [Alice, Bob]
println(withCharlie)  // [Alice, Bob, Charlie]
println(withoutAlice) // [Bob]

The original list remains unchanged.

Copy-on-write with sets

val roles = setOf("USER", "ADMIN")

val updatedRoles = roles + "AUDITOR"
val reducedRoles = roles - "ADMIN"

println(roles)        // [USER, ADMIN]
println(updatedRoles) // [USER, ADMIN, AUDITOR]
println(reducedRoles) // [USER]

Copy-on-write with maps

val settings = mapOf(
    "theme" to "dark",
    "language" to "en"
)

val updatedSettings = settings + ("theme" to "light")
val removedSetting = settings - "language"

println(settings)
// {theme=dark, language=en}

println(updatedSettings)
// {theme=light, language=en}

println(removedSetting)
// {theme=dark}

Updating objects with immutable collections

This pattern is especially useful with data class and copy():

data class Team(
    val name: String,
    val members: List<String>
)

val team = Team(
    name = "Backend",
    members = listOf("Alice", "Bob")
)

val updatedTeam = team.copy(
    members = team.members + "Charlie"
)

println(team)
// Team(name=Backend, members=[Alice, Bob])

println(updatedTeam)
// Team(name=Backend, members=[Alice, Bob, Charlie])

Avoid leaking mutable collections

Be careful with this:

val mutableUsers = mutableListOf("Alice", "Bob")
val users: List<String> = mutableUsers

mutableUsers += "Charlie"

println(users) // [Alice, Bob, Charlie]

Even though users is typed as List<String>, the underlying collection is still mutable.

If you need a defensive copy:

val users: List<String> = mutableUsers.toList()

Encapsulate mutation internally

A common pattern is to keep a mutable collection private, but expose a read-only copy:

class UserRegistry {
    private val users = mutableListOf<String>()

    fun addUser(user: String) {
        users += user
    }

    fun users(): List<String> = users.toList()
}

This prevents external code from modifying your internal list.

Prefer immutable state updates

For state management, prefer this style:

data class AppState(
    val selectedIds: Set<Int>,
    val usersById: Map<Int, String>
)

val state = AppState(
    selectedIds = setOf(1, 2),
    usersById = mapOf(
        1 to "Alice",
        2 to "Bob"
    )
)

val newState = state.copy(
    selectedIds = state.selectedIds + 3,
    usersById = state.usersById + (3 to "Charlie")
)

Use persistent collections for heavy updates

Kotlin’s standard collections are read-only interfaces, but not deeply immutable persistent data structures.

If you frequently create modified copies of large collections, consider kotlinx.collections.immutable:

import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf

val users: PersistentList<String> = persistentListOf("Alice", "Bob")

val updatedUsers = users.add("Charlie")

println(users)        // [Alice, Bob]
println(updatedUsers) // [Alice, Bob, Charlie]

Persistent collections share internal structure, so copy-on-write-style updates are usually more efficient than copying a whole List.

Summary

Use:

val names: List<String> = listOf("Alice", "Bob")
val updated = names + "Charlie"

Instead of:

val names = mutableListOf("Alice", "Bob")
names += "Charlie"

General guidance:

  • Expose List, Set, and Map, not mutable types.
  • Use +, -, map, filter, and copy() to create updated values.
  • Avoid leaking mutable backing collections.
  • Use toList(), toSet(), or toMap() for defensive copies.
  • Use kotlinx.collections.immutable for efficient persistent immutable collections.