Struct vs Record vs Class — Choosing the Right Type in Modern C## (.NET 9/10 Edition)
TL;DR
In modern C#, developers have three primary ways to model data types:
classstructrecord
All three allow you to define custom types.
But they behave very differently in terms of:
• memory allocation
• copying semantics
• equality behavior
• mutability
• performance
Choosing the wrong one can lead to:
• unnecessary allocations
• performance regressions
• incorrect equality comparisons
• subtle bugs in distributed systems
In modern .NET architectures, understanding these differences is essential.
This article breaks down exactly when to use each type, and why.
The Core Difference: Value vs Reference Semantics
Before we examine each type individually, we must understand the most fundamental distinction in .NET.
Types fall into two categories:
Value Types
→ stored directly
→ copied by value
→ typically allocated on the stack
Reference Types
→ stored on the heap
→ copied by reference
→ multiple variables can point to the same instance
In C#:
struct → value type
class → reference type
record → reference type (by default)
Understanding this difference explains nearly everything else.
Class — The Default Choice
A class is the most common type in C#.
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}
Classes are reference types.
This means the variable does not store the object itself — it stores a reference to the object.
Example:
var user1 = new User { Name = "Alice", Age = 30 };
var user2 = user1;
user2.Name = "Bob";
Console.WriteLine(user1.Name);
Output:
Bob
Why?
Because both variables point to the same object in memory.
Memory diagram:
Stack
├─ user1 ─┐
└─ user2 ─┘
Heap
└─ User instance { Name = "Bob", Age = 30 }
This behavior makes classes ideal for:
• domain entities
• mutable state
• complex object graphs
• dependency injection models
• application services
Classes represent identity.
Two objects may contain identical values but still represent different instances.
Example:
var a = new User { Name = "Alice", Age = 30 };
var b = new User { Name = "Alice", Age = 30 };
Console.WriteLine(a == b);
Result:
false
Even though the values match, they are different objects.
Struct — Lightweight Value Types
A struct behaves very differently.
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
Structs are value types.
This means the value itself is stored directly in memory.
Example:
var p1 = new Point { X = 10, Y = 20 };
var p2 = p1;
p2.X = 50;
Console.WriteLine(p1.X);
Output:
10
Why?
Because p2 received a copy of the value, not a reference.
Memory diagram:
Stack
├─ p1 { X=10, Y=20 }
└─ p2 { X=50, Y=20 }
This copying behavior makes structs extremely fast for small data types.
Common examples in .NET:
int
double
DateTime
Guid
Vector2
Span<T>
These types are structs because they represent simple values.
When Structs Become Dangerous
Structs are powerful, but they must be used carefully.
Bad example:
public struct LargeObject
{
public long A;
public long B;
public long C;
public long D;
public long E;
}
Now imagine passing this struct through many method calls.
Each call copies the entire object.
Method → copy
Method → copy
Method → copy
This can become more expensive than heap allocation.
Guideline used by many .NET engineers:
If a struct is larger than 16 bytes, reconsider using a class.
Another limitation:
Structs cannot inherit from other structs or classes.
They also cannot participate in complex inheritance hierarchies.
Record — Designed for Data Models
Records were introduced to simplify data-oriented programming.
Example:
public record User(string Name, int Age);
This single line creates:
• immutable properties
• value-based equality
• a constructor
• deconstruction support
• a ToString() implementation
Records are reference types by default, but behave like value types when comparing equality.
Example:
var a = new User("Alice", 30);
var b = new User("Alice", 30);
Console.WriteLine(a == b);
Output:
true
Even though they are separate instances, records compare by value.
This behavior is ideal for:
• DTOs
• API models
• event messages
• immutable data objects
Immutability — The Hidden Power of Records
Records encourage immutability.
Example:
public record Order(Guid Id, decimal Total);
Updating the object produces a new instance.
var order = new Order(Guid.NewGuid(), 100);
var updated = order with { Total = 120 };
The original object remains unchanged.
This pattern is extremely valuable in:
• distributed systems
• event sourcing
• functional programming models
• multi-threaded applications
Immutability eliminates entire classes of bugs.
Record Structs
Modern C## also supports record structs.
Example:
public readonly record struct Coordinate(int X, int Y);
This combines:
• value-type performance
• record-style equality
• immutable semantics
This is useful for high-performance data models.
Example use cases:
• game engines
• financial calculations
• physics simulations
Performance Considerations
Understanding memory behavior is critical for performance-sensitive applications.
Comparison summary:
Class
→ heap allocation
→ reference semantics
→ mutable by default
Struct
→ stack allocation (usually)
→ value semantics
→ copied by value
Record
→ reference type
→ value-based equality
→ immutable by design
Performance implications:
Class
+ flexible
+ supports inheritance
- extra allocations
Struct
+ zero heap allocations
+ fast for small values
- expensive when large
Record
+ concise syntax
+ safe immutability
+ value equality
Real Architecture Examples
Domain Entity
Use a class.
public class Order
{
public Guid Id { get; set; }
public decimal Total { get; set; }
}
Domain entities have identity and mutable state.
DTO / API Response
Use a record.
public record OrderDto(Guid Id, decimal Total);
DTOs represent data snapshots.
Value equality makes sense here.
Mathematical Value
Use a struct.
public readonly struct Vector2
{
public float X { get; }
public float Y { get; }
}
Small immutable values benefit from value semantics.
Decision Framework
A practical decision rule:
Is the object large?
→ Use class
Is the object small and immutable?
→ Use struct
Is the object a data model?
→ Use record
Another way to think about it:
Class → identity
Struct → value
Record → data
Why This Matters in Modern .NET
As .NET systems become:
• distributed
• concurrent
• event-driven
• high-performance
The distinction between value semantics and reference semantics becomes critical.
Incorrect choices can cause:
• unnecessary allocations
• synchronization bugs
• memory pressure
• subtle equality errors
Expert .NET developers treat type selection as an architectural decision.
Final Thoughts
The question is not:
Which type is best?
The real question is:
Which semantics match your data model?
class gives you identity and flexibility.
struct gives you performance and value semantics.
record gives you immutable data models with value equality.
Understanding these differences is one of the quiet skills that separates intermediate developers from experienced .NET engineers.
Written by Cristian Sifuentes
Software Engineer · .NET Architecture · Distributed Systems

Top comments (0)