Build Immutable Data Structures in Scala

Learn to build immutable data structures in Scala with this hands-on lesson for Python developers. Step-by-step guidance, comparisons, and troubleshooting included.

Focus: build immutable data structures in scala

Sponsored

You’ve been building data pipelines in Python, and everything works—until a teammate mutates a shared list and your entire pipeline produces garbage. Python’s mutable defaults and shared references can make bugs subtle and hard to trace. In Scala, you can build immutable data structures that eliminate an entire class of concurrency and state-management bugs. This lesson shows you how to construct and work with immutable data structures in Scala, drawing on your Python experience to make the transition fast and practical.

The problem this lesson solves

Python’s list, dict, and set are mutable by default. When multiple functions or threads share them, any mutation can silently corrupt state. You might have resorted to copy.deepcopy() or defensive copying, but those are slow and easy to forget. The core problem: mutable shared state makes your code harder to reason about, test, and parallelize.

In Scala, immutability is a first-class design choice. You can build data structures that are persistent — they share structure internally, so "changing" a value creates a new version without copying the whole thing. This means you get safety and performance. If you’ve used Python’s tuple or frozenset, you’ve tasted immutability, but Scala takes it much further.

Core concept / mental model

Think of Scala’s immutable collections as values, not objects. In Python, you might do:

lst = [1, 2, 3]
lst.append(4)  # mutates the same list

In Scala, you never mutate. Instead, you derive a new collection:

val lst = List(1, 2, 3)
val lst2 = lst :+ 4  // new list, original unchanged

This is possible because Scala collections use structural sharing. When you append to a linked list, the new list reuses the old tail. Only the new head is allocated. The old list remains fully intact, so it’s safe to share across threads or scopes.

Another key concept: persistent data structures. They preserve all previous versions of themselves, which is great for undo/rollback or audit trails. In Python, you’d have to write custom versioning; in Scala, it’s automatic.

How it works step by step

Step 1: Prefer val over var

In Scala, val declares an immutable reference, while var allows reassignment. For immutable collections, always use val. This is like a final variable in Python (though Python doesn’t enforce it).

Step 2: Use the standard immutable collection hierarchy

Scala’s default collections in scala.collection.immutable are immutable. The most common are List, Vector, Set, and Map. List is a singly-linked list optimized for head access; Vector is a tree structure with fast random access; Set and Map are hash tries—good for lookups.

Step 3: Know the operations that produce new collections

Instead of +=, use :+ (append), +: (prepend), updated, removed, etc. These return new collections, leaving the original untouched.

Step 4: Combine immutability with pattern matching

Immutable data structures work beautifully with pattern matching. You can destructure a List or a Map and process it functionally.

Hands-on walkthrough

Let’s start with a simple Python-to-Scala comparison, then write a small immutable data store.

Example 1: Python mutable vs Scala immutable

Python:

# Python: mutable list
data = [1, 2, 3]
data.append(4)
print(data)  # [1, 2, 3, 4]

Scala:

// Scala: immutable list
val data = List(1, 2, 3)
val updated = data :+ 4
println(data)     // List(1, 2, 3)
println(updated)  // List(1, 2, 3, 4)

Notice how data stays unchanged. This is the essence of immutability.

Example 2: Build an immutable configuration map

// Immutable Map
val config = Map(
  "host" -> "localhost",
  "port" -> 8080
)

// "Update" by creating a new map
val updatedConfig = config.updated("port", 9090)

println(config)        // Map(host -> localhost, port -> 8080)
println(updatedConfig) // Map(host -> localhost, port -> 9090)

Example 3: A custom immutable case class

For domain objects, define a case class—it gives you equals, hashCode, and copy for free.

case class User(id: Int, name: String)

val alice = User(1, "Alice")
val aliceRenamed = alice.copy(name = "Alicia")

println(alice)        // User(1,Alice)
println(aliceRenamed) // User(1,Alicia)

Example 4: Process a list with map/filter/fold

val numbers = List(1, 2, 3, 4, 5)
val evens = numbers.filter(_ % 2 == 0)
val doubled = evens.map(_ * 2)
val sum = doubled.sum

println(doubled) // List(4, 8)
println(sum)     // 12

In Python you’d use list comprehensions; in Scala, you use transformations that return new immutable collections.

Compare options / when to choose what

Data structure Best for Python equivalent Performance note
List Stack-like access, head/tail recursion list (but mutable) O(1) head cons, O(n) append
Vector Random access, large collections list with indices O(log n) indexed access, fast append
Set Uniqueness, membership tests set O(1) typical operations
Map Key-value lookups dict O(1) typical operations
case class Domain objects with immutability dataclass(frozen=True) Compile-time safety

When to use what: * Use List when you process from head to tail (functional algorithms). * Use Vector when you need fast random access. * Use Map for configurations or dictionaries. * Use case class for your own types that need to be immutable.

Troubleshooting & edge cases

Error: var used but not needed

If you find yourself writing var, ask: do I really need to reassign? Usually, you can restructure with val and transformations.

Error: "value :+ is not a member of ..."

If you’re using a mutable collection (e.g., scala.collection.mutable.ArrayBuffer), you won’t have :+. Stick to scala.collection.immutable imports.

Edge case: Large collections

Immutable List append is O(n) — if you need frequent appends, use Vector or build in reverse then reverse.

Edge case: Performance overhead

Structural sharing avoids full copies, but each “update” does allocate. For extremely hot paths, consider using Array or mutable pools, but isolate them behind an immutable API.

Pro tip: Use import scala.collection.immutable.{Vector, Map} explicitly to avoid accidentally pulling in mutable versions.

What you learned & what's next

You learned how to build immutable data structures in Scala: you can now create persistent collections, use case class for domain types, and transform data without mutating state. These skills make your code safer for concurrency and easier to reason about.

Lesson recap: * Immutability removes shared-state bugs. * Use val and the immutable collections: List, Vector, Map, Set. * Replace mutations with transformation methods (:+, updated, copy). * case class gives you immutable domain objects with useful methods. * Structural sharing makes immutability performant.

Next step: In the next lesson, you’ll dive into pattern matching and recursion, which build directly on immutable collections. You’ll learn how to write expressive algorithms that never mutate a single value.

Practice recap

Hands-on exercise: Write a small inventory tracker with Map[String, Int]. Define functions to add, remove, and update stock that return new maps instead of mutating. Then create a case class Product(id: Int, name: String, price: Double) and use .copy() to apply a discount. Test your functions in the Scala REPL and verify the original maps remain unchanged.

Common mistakes

  • Using var with immutable collections — this undermines immutability; use val and reassign (or better, avoid reassignment).
  • Forgetting that List append (:+) is O(n); use Vector for frequent appends.
  • Accidentally importing scala.collection.mutable and getting unexpected methods like +=.
  • Trying to modify a case class field directly — you must use .copy() to create a new instance.

Variations

  1. Use scala.collection.immutable.ArraySeq for an immutable indexed sequence with low overhead.
  2. Leverage cats.data.NonEmptyList from the Cats library for compile-time non-empty lists.
  3. Define your own immutable ADTs with sealed trait and case class for type-safe modeling.

Real-world use cases

  • Configuration management: immutable maps ensure no accidental mutation across threads.
  • Event sourcing: append-only logs of immutable domain events enable replay and audit.
  • Functional web services: immutable request/response DTOs reduce bugs in concurrent handlers.

Key takeaways

  • Immutability eliminates shared-state bugs and simplifies reasoning and concurrency.
  • Use val and immutable collections (List, Vector, Map, Set) by default.
  • Transformations like :+, updated, and copy return new collections without altering the original.
  • Structural sharing makes immutable operations efficient by reusing unchanged parts.
  • case class provides immutable, value-based domain objects with useful methods.
  • Choose the right structure: List for recursion, Vector for random access, Map/Set for lookups.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.