How it works
Immutability is a behavior contract: after construction, the externally meaningful state of an instance does not change. An ordinary class can follow that contract with validated construction and get-only properties. Records provide convenient generated features for many value models, but understanding the underlying design helps you reason about models that need custom construction or behavior.
An update operation on an immutable model returns another instance. The original remains available, which is useful for comparing revisions, retaining a prior result, or passing stable values between parts of a program. This does not automatically make every reachable object immutable. In this lesson the properties contain strings and decimals, so there is no nested mutable collection that would undermine the intended snapshot.
Walk through the example. PriceTag validates a nonnegative amount and stores the currency. WithAmount constructs another PriceTag using the same currency and a new amount, passing through the same validation boundary. The program prints the original and revised amounts separately, then confirms that they are different object instances. A revised amount therefore does not alter the earlier value.
Watch the boundary. A get-only property that returns a mutable list still exposes mutable reachable state. Immutability requires considering the whole public model, not just removing setters. Also avoid an update method that bypasses validation through a private back door; every constructed state should satisfy the same invariant. When a model becomes complicated, centralize its validation rather than duplicating slightly different rules across copy methods.
Practice independently. Add a WithCurrency operation with an explicit rule for supported currency codes. Verify that all update methods leave earlier values unchanged and reject invalid state consistently. Then add a nested list in a separate experiment and demonstrate how a shallow copy can share it. Explain whether an independent collection copy or an immutable collection would better preserve the intended contract.
A working example
using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
var original = new PriceTag("USD", 12m);
var revised = original.WithAmount(15m);
Console.WriteLine(original.Amount.ToString("F2", CultureInfo.InvariantCulture));
Console.WriteLine(revised.Amount.ToString("F2", CultureInfo.InvariantCulture));
Console.WriteLine(ReferenceEquals(original, revised));
sealed class PriceTag
{
public string Currency { get; }
public decimal Amount { get; }
public PriceTag(string currency, decimal amount)
{
if (amount < 0) throw new ArgumentOutOfRangeException(nameof(amount));
Currency = currency; Amount = amount;
}
public PriceTag WithAmount(decimal amount) => new PriceTag(Currency, amount);
}
Expected output
12.00 15.00 False
An immutable update constructs another valid instance and preserves the earlier value.