The Value of Immutability in .NET
Introduction
Immutability - the state or quality of being unchangeable, unalterable, or unable to be modified over time.
In software development, immutability generally refers to an object whose values can’t be changed by the system once created. I was in a discussion this morning where we were talking a bit about the value of immutability in software, and why it’s such an important concept. I figured it was worth delving into a bit for a post. As a software developer, until you have experienced the pain of trying to track down a bug that’s tied to a piece of data that got changed at some point in the process, you have a hard time accepting the value of it in your code.
What Is Immutability?
I stated that in code it refers to an object whose values can’t be changed. What does that look like in C#? Let’s look at an example:
public class MutableAddress {
public string Street1 { get; set; }
public string Street2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
}
public class ImmutableAddress {
public string Street1 { get; init; }
public string Street2 { get; init; }
public string City { get; init; }
public string State { get; init; }
public string Zip { get; init; }
}
There are a number of ways to make an object immutable in C# code. The above example is just one of them. In the MutableAddress class, the properties all have a set accessor, letting the code update their values at will. In the ImmutableAddress class, the properties all have the the init accessor. That means that the values can only be set at the moment that an instance of the class is create, in a constructor. After the constructor runs, those values cannot be changed.
There are other ways to do it as well, such as making the setters private, or removing them entirely, or making the fields readonly.
public class ImmutableAddress {
public string Street1 { get; }
public string Street2 { get; private set; }
public readonly string City { get; }
}
You might also make the class sealed to prevent the class from being inherited and thus introducing means of adding mutable behavior in the inherited class.
There is also the record type, which is immutable by default. I say “by default” because there are ways to make a record mutable. But at it’s base, it’s immutable and makes a good alternative to using classes. There used to be a more distinct difference between records and classes. But over the last few years, they’ve kind of drifted a bit closer together.
public record ImmutableAddress(string Street1, string Street2, string City, string State, string Zip)
There are also a variety of collection types that are immutable by design such as ImmutableList<T> and ImmutableDictionary<T> that provide you with the means of creating lists that cannot be altered once created if you need to work with multiple objects together.
But all that’s about how you can make a class or its values immutable. The thing that’s more important to understand is why you would want to do so.
The Problems Mutability Creates
So why do we want to make objects immutable whenever possible. There are a number of problems that mutability creates in code.
An object in code is generally not the source of truth for that piece of data. That data has a permanent home somewhere, likely a database of some kind in most instances. And while that objects “lives” somewhere else, like in your code, changes to it aren’t really valid until they get back to that permanent home. By making the objects immutable, you’re forcing yourself to better address this by making your code update the source of truth as soon as possible after a change is made. It’s a matter of data integrity. And the sooner that data gets updated to the source of truth, the better your data integrity is across the board.
Immutable objects are natively thread-safe. If your code is running multiple threads, and that object is mutable and changeable, you can run into race conditions and the need to create complex synchronization mechanisms to insure that all the threads have the correct data values.
It’s predictable. If the state of an object is guaranteed to be consistent throughout its lifetime, you minimize the risk of side-effects from your code causing difficult to track down bugs.
It’s safe to share. You can pass them around, even make copies if need be, without worrying about what a particular block of code is doing to the original object. There won’t be any unexpected changes.
It makes caching easier. If you can’t update an object, you don’t need to come up with ways to synchronize changes to the value in the cache.
Memory usage is more efficient. The compiler is able to make better and more efficient use of memory space if it knows that the size of the values isn’t going to change. Once an immutable object is placed into the computer’s memory, it is much less likely to ever need to be moved within the memory space.
Common Objections (and Rebuttals)
There are some common objections to immutable objects, but none of them really hold water. For instance, if you do need to update a value, you need to create a new version of the object. It used to be, long ago, that doing so was expensive compute-wise. But these days that’s no longer the case. Creating a new object has virtually no performance impact on the system.
Another argument is “we use a stateful domain model”. That’s fine. Create a new version, persist it back to the source of truth, and move on.
“It involves too much refactoring and rewriting of legacy code”. That code needs to go away. Saying it’s too hard to clean up bad code is never an excuse to not do something.
“It adds complexity!” Sure, maybe a little tiny bit. But it really doesn’t add anything to the overall complexity of any modern application. It’s just a different way of thinking.
Practical Patterns and Pitfalls
Patterns
So what are the best practices for using immutable objects? One of the biggest questions is: How do I easily create a new version of the object with updated values? There are a couple of approaches to doing this, depending on the situation.
One common method is to use the “Builder Pattern”. This invovles having a record as our immutable object and then a “builder” class that can be called to create a new version of the record for when we need it. Something like this:
public record Address(string Street1, string Street2, string City, string State, string Zip);
public class AddressBuilder{
public string Street1 { get; set; } = new();
public string? Street2 { get; set; } = new();
public string City { get; set; } = new();
public string State { get; set; } = new();
public string Zip { get; set; } = new();
public AddressBuilder(string street1, string city, string state, string zip, string street2 = null){
Street1 = street1;
Street2 = street2;
City = city;
State = state;
Zip = zip;
}
public virtual Address Build(){
if(string.IsNullOrEmpty(Street1)) throw new ValidationException("Street1 must have a value");
return new Address(this.Street1, this.Street2, this.City, this.State, this.Zip);
}
}
Your builder class and the Build() function allow you to encapsulate all of the validation needed before creating and returning the new record. That makes it as easy as something like:
var newAddress = new AddressBuilder("123 Any st", "MyCity", "ST", "12345").Build(); //creates a new Address record
There’s more to the Builder Pattern, but you get the idea.
If you don’t need the validation, record has a built-in functionality in the with expression.
Address newAddress = currentAddress with { Street1 = "New street value" };
In this example, newAddress gets created as a new record object with all the same data values as currentAddress, except that Street1 gets replaced with “New street value”.
Pitfalls to Avoid
So what pitfalls do we need to avoid. First, while the performance “hit” of creating a new object is negligible in modern architectures, there is still a performance cost. If you’re dealing with a situation where you’re looping through vast numbers of objects creating new instances, that can add up over time. Immutable isn’t the best option for those situations.
Another situation is shallow vs deep immutability. Shallow immutability refers to the fact that while the fields in an immutable object cannot be changed, the objects that they point to might still be mutable. For example, if my Address record contains a reference to an instance of a List<Phone>, the reference to that list can’t be changed, but the list could still be changed. So it’s important to think about what objects your immutable class or record contains and whether or not those objects should be allowed to change. If you ensure that all of the objects all the way down the structure of an immutable object are also immutable, then it is understood to have deep immutability.
Real-World Use Cases & When To Use Immutable vs Mutable
So, when is it better to use immutable objects and when should we use mutable objects? That’s a matter of frequent debate amongst the developer community. But in general, it’s considered best practices these days to use immutable by default in most situations. It really comes down to the likelihood that an object will need to change and how many changes are likely to occur after the object is created. Dates, addresses, information about people, statistics, historical information, and so forth are all good examples of data that changes very rarely and should all be immutable.
Real-time data about the current state of a mechanical device like temperatures, RPMs, voltage, and the like change rapidly and frequently and would likely be stored in a mutable object. State management objects are another good use case for mutable objects.
One case to keep in mind in the internal workings of C#: strings. Strings are always immutable. Well, behind the scenes anyway. If you change the value of a string, the system actually creates a new instance of a string with the new value and replaces the reference to it, leaving the old one for garbage collection. That goes back to the early days of the language and how memory management works. It just has always been that way. Fun fact.
Conclusion
While it’s true that in the early days of C#, performance meant that immutable was a less desirable option. These days, however, that has largely changed and the benefits of immutable objects far outweigh the downsides. As you consider your architecture, tend towards immutable objects unless the situation calls for a lot of change.

