Introduction
If you’ve spent any time building C# applications, you’ve probably run into situations where you need to decide whether your objects should be changeable or locked down after creation. This choice between mutability and immutability isn’t just academic, it can significantly impact how your code behaves, how easy it is to debug, and even how it performs.
In this post, I’ll walk you through both approaches and help you figure out which one makes sense for your specific coding challenges.
What is Immutability?
At its core, immutability is pretty straightforward: once you create an object, you can’t change it. Period. If you want to modify something, you create a brand new object with your changes instead.
Think of it like working with sticky notes. Rather than erasing what’s on a note, you just grab a fresh one, copy over what you want to keep, add your changes, and toss the old note.
Why Immutability Can Be Great
I’ve found immutable objects incredibly useful in many situations, and here’s why:
They’re naturally thread-safe. No need to worry about one thread changing an object while another is reading it, it simply can’t happen.
What you see is what you get. Once an object exists, you know its state won’t mysteriously change behind your back.
Debugging becomes simpler. When tracking down issues, you don’t have to figure out where an object might have been modified.
Caching works better. Cache an immutable object and it stays exactly as you put it in.
Security improves. No one can tamper with important data after it’s created.
They make great dictionary keys. Since their hash codes never change, they won’t get “lost” in dictionaries.
How to Create Immutable Objects in C#
So how do you actually build these unchangeable objects in C#? Here are some straightforward approaches I use regularly:
1. The readonly Keyword: Your First Line of Defense
The readonly
keyword is one of the simplest ways to make fields immutable:
public class Temperature
{
public readonly double Value;
public readonly string Scale;
public Temperature(double value, string scale)
{
Value = value;
Scale = scale;
}
// Need to convert? Just make a new object
public Temperature ToCelsius()
{
if (Scale == "Celsius")
return this; // No change needed
double celsiusValue = (Value - 32) * 5 / 9;
return new Temperature(celsiusValue, "Celsius");
}
}
What’s neat about this approach is how clearly it communicates intent, you’re explicitly saying “this value won’t change after initialization.”
2. Properties with Private Setters: Fine-Grained Control
Another common pattern uses properties that only allow changes from within the class:
public class Person
{
// Modern C# 9.0+ init-only property
public string FirstName { get; init; }
// Classic approach -> private setter
public string LastName { get; private set; }
// Computed property -> recalculated on demand
public string FullName => $"{FirstName} {LastName}";
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
// Want to change a value? Return a new Person
public Person WithLastName(string newLastName)
{
return new Person(FirstName, newLastName);
}
}
Notice the pattern? Instead of modifying properties, you create methods that return new objects with your changes applied.
3. Records: Immutability Made Easy (C# 9.0+)
If you’re using newer versions of C#, records are a game-changer for immutability:
// This simple line creates an entire immutable class!
public record Customer(string Name, string Email, int CustomerLevel);
// Using it is intuitive
var customer = new Customer("John Doe", "john@example.com", 2);
// The "with" syntax makes creating modified copies elegant
var updatedCustomer = customer with { Email = "john.doe@example.com" };
Records automatically implement value-based equality and other goodies that make immutable objects pleasant to work with.
4. Sealed Classes: Locking Down Your Design
Sometimes you need to prevent inheritance to maintain immutability:
public sealed class Configuration
{
public string ApiKey { get; }
public int Timeout { get; }
public Configuration(string apiKey, int timeout)
{
ApiKey = apiKey;
Timeout = timeout;
}
}
The sealed
keyword prevents someone from creating a derived class that might accidentally make your immutable class mutable.
5. Immutable Collections: When Lists Need to Be Locked Down
Regular collections in C# are mutable by default, but there’s a whole set of immutable alternatives:
using System.Collections.Immutable;
// Starting with some names
var names = ImmutableList.Create("Alice", "Bob");
// "Adding" actually creates a new list
var moreNames = names.Add("Charlie");
// The original is untouched
Console.WriteLine(string.Join(", ", names)); // Alice, Bob
Console.WriteLine(string.Join(", ", moreNames)); // Alice, Bob, Charlie
These collections are perfect when you need to ensure nobody can modify your list, array, dictionary, or other collection type.
What About Mutability?
On the flip side, we have mutable objects, the ones that can change after creation. This is actually the default in C#, and there’s a good reason for that. Sometimes you actually want things to change!
When Mutability Shines
While immutability has its cheerleaders (myself included in many cases), mutability has some real advantages:
It’s often simpler, especially for beginners. Changing an existing object is an intuitive concept.
It can be faster, creating new objects constantly has performance costs that add up.
It’s memory-efficient, fewer objects means less garbage collection, which can really matter in high-performance scenarios.
Some problems just fit better, certain algorithms and data structures naturally work with changing state.
A Simple Mutable Example
Here’s a shopping cart that shows mutability in action:
public class ShoppingCart
{
public List<string> Items { get; private set; } = new List<string>();
public decimal TotalPrice { get; private set; } = 0;
public void AddItem(string item, decimal price)
{
Items.Add(item);
TotalPrice += price;
}
public void RemoveItem(string item, decimal price)
{
if (Items.Remove(item))
{
TotalPrice -= price;
}
}
public void Clear()
{
Items.Clear();
TotalPrice = 0;
}
}
This makes perfect sense as a mutable object, it models a real shopping cart where you’re constantly adding and removing items.
The Gotchas with Mutable Objects
Before you go all-in on mutability, there are some headaches it can cause:
Thread safety becomes your problem - when multiple threads access the same mutable object, you need locks or other synchronization.
You might need defensive copies - passing a mutable object to another component means they can change your data.
Side effects can bite you - change an object in one place, and it might unexpectedly affect code elsewhere.
Debugging gets trickier - when objects can change anywhere, it’s harder to track down exactly where a problematic change happened.
Let’s Compare: A Tale of Two User Profiles
To really see the difference in action, let’s look at two ways of handling the same concept, a user profile with roles:
The Immutable Way
// Immutable User class
public class User
{
public string Username { get; }
public string Email { get; }
public ImmutableList<string> Roles { get; }
public User(string username, string email, IEnumerable<string> roles)
{
Username = username;
Email = email;
Roles = ImmutableList.CreateRange(roles ?? Array.Empty<string>());
}
// Need to change something? Get a new user!
public User WithEmail(string newEmail)
{
return new User(Username, newEmail, Roles);
}
public User AddRole(string role)
{
return new User(Username, Email, Roles.Add(role));
}
}
// How you'd use it
var user = new User("johndoe", "john@example.com", new[] { "user" });
// Changes create new objects, original stays untouched
var adminUser = user.AddRole("admin").WithEmail("john.admin@example.com");
// Now we have two separate objects
// user → still has original email and just "user" role
// adminUser → has new email and both "user" and "admin" roles
Notice the flow? Each change gives you back a new object, leaving the original untouched. This creates a clean history of changes and prevents unexpected side effects.
The Mutable Way
Now let’s see the same concept with traditional mutable objects:
// Mutable User class
public class MutableUser
{
public string Username { get; set; }
public string Email { get; set; }
public List<string> Roles { get; } = new List<string>();
public MutableUser(string username, string email)
{
Username = username;
Email = email;
}
public void AddRole(string role)
{
Roles.Add(role);
}
public void RemoveRole(string role)
{
Roles.Remove(role);
}
}
// How you'd use it
var user = new MutableUser("johndoe", "john@example.com");
user.AddRole("user");
// Now we modify the original directly
user.AddRole("admin");
user.Email = "john.admin@example.com";
// Just one object that's been changed in place
This feels more direct, we just change what we have. No need to track multiple objects, and it can be more efficient since we’re not creating new objects.
So When Should You Go Immutable?
After years of working with both styles, here’s when I reach for immutability:
When multiple threads are involved, Immutability means no locks needed, and that’s a huge win.
For “value” types of objects, Things like Money, DateRange, or Address that represent a specific value are natural fits for immutability.
For configuration, Settings that should remain constant after startup are perfect immutability candidates.
When returning data from APIs, You don’t want callers accidentally modifying your internal state.
When embracing functional patterns, If you’re writing more functional-style C#, immutability is your friend.
And When is Mutability Better?
Mutability still has its place in my toolbox for these scenarios:
Performance-critical code, When you need every CPU cycle and memory byte you can get.
Builder patterns, When you’re constructing something complex bit by bit before finalizing it.
Naturally stateful objects, Game entities, UI components, or anything that’s inherently about tracking changing state.
Local-scope objects, When an object lives briefly in a method and isn’t shared anywhere else.
The Practical Take
Here’s what I’ve learned from building production C# systems: don’t be dogmatic. Most good C# codebases I’ve worked on use both approaches where they make sense.
I generally start with immutability as my default for domain objects and public APIs. It prevents a whole class of bugs and makes code easier to reason about. Then I use mutability where it offers clear benefits, typically for performance reasons or when the problem naturally expresses itself that way.
For example, in a typical web application, I might make my:
- Domain entities immutable (Customer, Order)
- DTOs and API responses immutable
- View models mutable
- Complex builders and factories mutable
- Configuration settings immutable
The best part about C# is that it gives you the tools to work both ways. Unlike some languages that push you strongly toward one approach, C# lets you decide what makes sense for each part of your application.
So rather than picking sides in some abstract “immutability vs. mutability” debate, pick the right tool for each job. Your future self (and teammates) will thank you when they’re trying to understand your code at 2 AM during a production issue!