Dataslope logoDataslope

Immutability

Why values that never change make programs simpler, safer, and easier to compose — and how modern C# expresses immutability ergonomically

A value is immutable if it cannot change after it is constructed. Once you create the Point { X=1, Y=2 }, it stays Point { X=1, Y=2 } forever. To get a "modified" version, you build a new point.

This sounds wasteful. It is not. It is the single most reliable weapon against an entire family of bugs that every programmer has written at least once — and once you stop fighting it, you'll find your code gets simpler, not more complex.

The bug that immutability prevents

Here is a tiny "shopping cart" written with a mutable list. Look at what happens.

Code Block
C# 13

The caller passed in a cart with three items. The function computed the total — and silently added a fourth item. Now anywhere else in the program that touches myCart sees four items. That bug is called aliasing: two pieces of code share a reference to the same mutable object, and one of them quietly changes what the other is reading.

Immutable data eliminates aliasing as a bug class. If cart cannot be changed, it doesn't matter who else holds a reference to it.

Immutable values in modern C#

C# has multiple tools for immutability, each at a different level.

Records (the workhorse)

A record is a class designed for immutability. By default its positional properties are read-only and equality is by value.

Code Block
C# 13

a is unchanged. c is a brand-new record. Equality compares contents, not references — which is what you want for value types.

readonly fields and init setters

For classes (not records), use init for properties that should be set once during construction:

Code Block
C# 13

Immutable collections

The mutable defaults — List<T>, Dictionary<TKey,TValue>, HashSet<T> — have immutable cousins in System.Collections.Immutable. An immutable list returns a new list when you "add" to it.

Code Block
C# 13

Each call returns a new structure. Internally, the immutable collections share storage so this isn't as expensive as it looks.

Non-destructive update

Building a new value from an old one is called non-destructive update. The with expression is its idiomatic form for records.

Code Block
C# 13

Notice the pattern: each "transition" is a new value. The previous states still exist. This is incredibly useful for undo, time travel, debugging, and persistence.

The performance question

The most common objection to immutability is "isn't allocating new objects expensive?". The honest answer:

  • For small records and short pipelines, no. Modern GC is optimized for short-lived allocations.
  • For very large collections updated millions of times per second, yes — and that's why C# offers both immutable and mutable collections, and Span<T> for hot loops.
  • Most "performance fixes" by switching to mutation come at the cost of bugs that take far more time to diagnose than they save.

The rule of thumb is: start immutable, mutate only after a profiler proves it matters.

A small map of "mutability levels"

The closer to the top, the easier it is to reason about. A record that contains a mutable List<int> is not really immutable — you can still call .Add on the inner list. True immutability requires "immutability all the way down".

Immutability inside an OO design

You don't have to make every class a record. A practical rule:

  • Values (Money, Point, Email, OrderLine) → immutable records.
  • Entities (the User who logs in, the Order being modified by many requests) → mutable, but with controlled mutation through a small set of methods.
  • Services (the OrderService that processes things) → typically stateless or have only configuration as state.

Most of the bugs in a typical codebase live in the values, not the services. Making values immutable is where the biggest win is.

A multi-file challenge

Implement a Money record and an Inventory service that uses it without mutating it.

Challenge
C# 13
Immutable Money with a pure 'Add'

Define a record Money(decimal Amount, string Currency) in Money.cs. In the same file, implement a method:

public static Money Add(Money a, Money b)

…that returns a new Money with the summed amount, only if the currencies match. If currencies differ, throw System.InvalidOperationException.

The given Program.cs will call your method and print the result.

QuestionSelect one

Which statement best captures the relationship between immutability and aliasing bugs?

Immutability is only useful in multi-threaded programs.

Aliasing bugs are easily prevented by adding more unit tests.

Immutability is mainly a matter of code style.

Immutable values cannot be aliased into incorrect states, because nothing can change them after construction.

On this page