Records and Classes in C# and When Each One Fits
When records arrived in C#, they were widely misunderstood as merely a shorter way to write classes — less boilerplate for the same thing. That framing misses the point entirely and leads developers to use them in the wrong places. Records are not a syntactic convenience for classes; they represent a different way of thinking about a type, built around a different notion of what a type's identity means. Understanding the real distinction between records and classes, rather than treating one as shorthand for the other, is what lets you choose the right tool and write code that expresses its intent clearly.
Identity versus value
The deepest difference between records and classes is philosophical before it is technical, and it concerns what it means for two instances to be "equal." A class, by default, has reference identity: two class instances are considered equal only if they are the same object in memory, regardless of whether their contents are identical. Two separately created class objects holding the exact same data are, by default, not equal, because equality is about identity — being the same thing, not looking the same.
A record inverts this. Records have value equality: two records are considered equal if their contents are equal, regardless of whether they are the same object in memory. Two separately created records holding the same data are equal, because for a record, identity is defined by value, not by reference. This is the crux of the whole distinction. A class answers the question "are these the same object?"; a record answers "do these represent the same value?" Everything else about records flows from this single, fundamental choice, and it is the question you should ask when deciding between them: does identity for this type mean sameness of object, or sameness of value?
What each is built for
This difference in equality maps onto a difference in purpose. Classes are built to model objects with identity and behaviour — entities that have a distinct existence, that change over time, and that are defined by who they are rather than merely what they contain. A customer, a service, a connection, a stateful component: these are things where two instances with the same data are genuinely different things, and where mutating state and encapsulating behaviour are central. The reference identity of a class fits this perfectly.
Records are built to model data — immutable values defined entirely by their contents, where two instances with the same data are, for all purposes, the same. A point in space, a piece of a data transfer object, a message, a configuration value, a result: these are things you care about for what they hold, not for their identity as objects, and where equality by value is exactly what you want. Records are ideal for the immutable data that flows through an application, and classes for the stateful, behaviour-bearing objects that constitute its structure. Choosing between them, then, is really choosing whether you are modelling a thing or a value.
Immutability and non-destructive change
Records are designed with immutability in mind, and this aligns naturally with their value semantics. An immutable type, once created, does not change; if you need a different value, you create a new one. This might sound restrictive, but for data it is a significant strength, because immutable data is far easier to reason about — it cannot be altered unexpectedly by code elsewhere, it is safe to share across threads, and it eliminates a whole category of bugs caused by unintended mutation.
To make working with immutable values practical, records support non-destructive mutation: the ability to create a new record that is a copy of an existing one with certain values changed, leaving the original untouched. This gives you the convenience of "modifying" a value while preserving the guarantees of immutability, because what you actually get is a new instance rather than a change to the old one. This pattern — deriving new values from old ones rather than mutating in place — is central to how records are meant to be used, and it is what makes immutable data ergonomic rather than cumbersome. It fits the value-oriented mindset: a value is what it is, and a different value is a different thing.
Where the choice gets subtle
The distinction is clean in the obvious cases but requires judgement at the edges, and a couple of clarifications help. First, records come in both reference and value varieties, so the choice is not simply "record versus class" but involves how the type is stored as well as how equality works; the value-equality behaviour is the defining trait regardless. Second, it is possible to make a class immutable and to give it value-like behaviour manually, and it was common to do so before records existed — records simply make that pattern concise and built-in rather than hand-written boilerplate.
The practical guidance is to let the semantics drive the decision rather than the syntax. Ask whether the type is fundamentally a value, defined by its data, where two equal-contented instances should be equal and immutability is desirable — and if so, reach for a record. Ask whether it is fundamentally an entity or a service, with identity, state, and behaviour, where reference identity is correct — and if so, reach for a class. The mistake is choosing a record because it is shorter, or a class out of habit, without considering which model of identity actually fits the type. The right choice expresses intent: it tells the next reader whether this thing is a value or an object.
Why the distinction improves your code
Choosing correctly between records and classes does more than satisfy a stylistic preference; it makes code clearer and safer. When you use a record for data, you communicate immediately that the type is an immutable value with value equality, and readers can rely on those guarantees without checking. When you use a class for an entity, you signal identity and mutability. The type itself carries meaning about how it should be used, which reduces the mental overhead of understanding a codebase and prevents misuse.
There are also concrete correctness benefits. Using records for the immutable data that flows through an application — the messages, the results, the transfer objects — brings value equality where it is genuinely wanted, so comparisons and lookups behave intuitively, and immutability where it prevents bugs. Using classes for stateful, behaviour-rich objects keeps identity where it belongs. Matching the type to its true nature, rather than reaching for whichever is shorter or more familiar, produces code whose structure reflects its intent — the same clarity of design that good architecture aims for throughout an application. The distinction is small in syntax and large in meaning.
Conclusion
Records and classes are not two ways of writing the same thing; they embody two different answers to what a type's identity means. Classes have reference identity and are built for entities and services — things defined by who they are, with state and behaviour. Records have value equality and are built for immutable data — things defined by what they contain, where two equal-contented instances are genuinely the same value, and where immutability and non-destructive change bring real safety. Choosing between them is choosing whether you are modelling an object or a value, and letting that semantic question drive the decision, rather than reaching for whichever is shorter, produces code that expresses its intent and behaves as readers expect. Records were never merely a concise class. They are a distinct tool for a distinct job, and using each where it fits is one of the quiet marks of well-designed C#.


