Design & Architecture

Mutable vs Immutable Design

Intermediate

When something changes, you have two choices: edit the existing thing in place (mutable), or leave it alone and produce a new version (immutable). This single decision shapes how easy your code is to reason about, test, parallelise, audit, and debug. Our strong default is immutability — favour it everywhere it is affordable, and treat in-place mutation as a deliberate, justified exception rather than the norm.

A mutable value can be changed after it is created: you update a field, push into a list, overwrite a row. An immutable value never changes once created — to "change" it you build a new value from the old one and leave the original untouched. Most bugs that are hard to reproduce — the ones that only happen "sometimes" — come from shared mutable state being changed from somewhere you did not expect.

This is a house preference, and a strong one. In a regulated, money-handling, multi-tenant system, the qualities immutability gives you — values you can trust not to change under you, a history you can reconstruct, state that is safe to share across threads — are worth the small extra cost of allocating new objects. This page sets the default; Event Sourcing, Data Integrity & Transactions, Audit Trails, and Concurrency apply it in specific places.

Default to immutable

Mutable entity edited in place screening.Status = "Clear"; // overwrites the only copy
screening.ReviewedBy = userId;
screening.ReviewedAt = DateTime.UtcNow;
// the previous decision, and who made it, are gone forever

The earlier decision is destroyed. In an AML context that is the history a regulator asks for — and it is unrecoverable.

Immutable record, new version public record ScreeningDecision(string Status, Guid ReviewedBy, DateTimeOffset ReviewedAt);

var cleared = decision with { Status = "Clear", ReviewedBy = userId, ReviewedAt = now };
store.Append(cleared); // the original decision is still on record

The new decision is derived from the old one; the old one is untouched and still queryable. History is preserved by construction.

Why immutable is the safer default

When mutability is the right call

Self-review checklist

Why it matters: Shared mutable state is the single biggest source of bugs that are hard to reproduce and hard to fix: race conditions, accidental aliasing, and history quietly overwritten. Defaulting to immutable makes code easier to reason about one piece at a time, safe to run in parallel, and honest about the past — which in a regulated, money-handling system is not a nicety but a requirement. We treat immutability as the default and in-place mutation as the exception that must earn its place.