← Home
.NETOOP and C# basicsReported by candidates

What is the difference between value types and reference types in C#? Where do they live (stack or heap), how is a struct different from a class, and how does it affect performance?

12 reports in the question index (7 linked below), mostly first technical rounds and online tests: EPAM, SS&C Technologies, Thinknext Technologies, Nvest Solutions and others. Often asked with "give code examples" or "how does it affect performance?".

Say this first · about 30 seconds
Make it yours · Navistar Financial truck lease mileage (invented example)
The code, simplified

Invented to show the shape, in C#. Tap Next to walk one reading of 41,237 miles through the fee job.

Navistar.sln · where these 2 files sit
  • Leases/
Diagram · step 1The job is called with the reading. It gets its own copy: 41,237 in two places now.
FeeJob.cs · line 4 · 1 of 3The end-of-lease job calls this with the truck's last reading, 41,237 miles; the portal built it with new MileageReading(41_237). reading is a copy: this method has its own.

Fix later in your repo Example written for this card: invented, not from a real codebase.

Say it out loud, then check

Show the 30-second answer
  • The difference is what a variable holds. A value type, like int, decimal, DateTime or any struct, holds the data itself, so assigning it copies the data. A reference type, like any class, string or array, holds a reference to an object on the heap, so assigning it copies only the reference, and both variables point at the same object.
  • That bit us once on our BMW FS lending API. A quote preview was written as a plain assignment, so changing the preview's term also changed the quote the customer had already been sent. The fix was to build a new object for the preview.
  • On stack versus heap: local value-type variables usually live on the stack, and objects live on the managed heap, where the garbage collector cleans them up. But a value type inside a class lives on the heap, inside that object. So I explain the difference by what gets copied, not by where it lives.
  • A struct is a value type and a class is a reference type. I use a struct only for small values that never change, like a mileage reading, and I make it a readonly struct, so nobody can change a copy by mistake. Anything with an identity that changes over time, like a contract or an instalment, is a class.
  • For performance, value types avoid heap allocations and garbage collection work, which matters in loops over millions of rows. The costs are copying big structs on every assignment, and boxing, where a value type is put into an object variable and gets a hidden copy on the heap.
Check your answer: tick what you said

You said 0 of 9

Then add your example: Navistar Financial truck lease mileage (invented example)
  • Situation: a Navistar Financial truck lease allows a set number of miles. Above it, the customer pays an excess-mileage fee when the lease ends. The truck's telematics box (the device that reports its miles) sends readings, and each reading was a small class, . The fee job rounded the miles on the object it was handed, and the portal showed that same object, so customers saw rounded miles.
  • What I did: made a readonly struct. It is a small value with no identity of its own, so every method that gets one gets its own copy, and makes changing it a compile error. The fee job now builds a new, rounded reading instead of changing the one it was given.
  • Result: the portal always shows the miles the truck reported, and the fee uses its own rounded copy. A test proves the original reading doesn't change.
  • What I'd say: a small value like a reading is a readonly struct: copied, never shared, never changed. The lease itself stays a class, because it has an identity and changes over its life.

See it

1/8int term = 36; An int is a value type: the variable holds the number itself.

In plain words

The senior answer · about 2 minutes
The difference, in my words
  • It is about what a variable holds. A value type holds the data, so every assignment and every method call makes a copy. A reference type holds a reference, so an assignment makes a second name for the same object.
  • On our BMW FS lending API, a quote preview was written as Quote preview = sent, and then the preview's term was changed. Both names pointed at one object, so the quote the customer had been sent changed too. The fix was a new object for the preview, and a test that checks the sent quote stays the same.
  • The everyday trap is method parameters. C# passes a copy of the variable. For a class that copy is the reference, so a method that "just calculates" can change the caller's object. That is the first bug below.
Stack and heap, honestly
  • The stack is a method's own short-lived space: its local variables go there, and it is gone when the method returns. Objects made with from a class go on the managed heap, and the frees them once nothing refers to them.
  • But "value types live on the stack" is only half true. A decimal field inside a lives inside the , on the heap. A boxed value lives on the heap. And a local variable that a lambda (a small inline function) captures, or that an method keeps across an ( and : code that waits without blocking), is moved by the compiler into an object on the heap. So I define the two kinds by what gets copied, and treat the location as the runtime's choice.
struct or class
  • I reach for a struct only when the type is one small value, never changes, and is used in large numbers: a mileage reading, a date range, a currency code. That matches Microsoft's guideline: under 16 bytes, immutable, rarely boxed. And it is always a readonly struct, so the compiler refuses any change.
  • Anything with an identity that changes over its life, like a contract, a payment or an instalment, is a class. We learned that in production: a mutable struct in a list silently lost every change made to it. That is the incident in Deep.
  • If I need value equality on a data type, I use a , or a readonly record struct (a small value type with built-in value equality) for a small value, instead of writing by hand.
Performance, and how I'd measure it
  • Value types avoid a heap allocation each, so less work for the garbage collector. An array of structs also sits in one block of memory, which is fast to loop over. That matters in a over millions of instalments, not in a controller that handles one request.
  • The costs: a big struct is copied on every assignment and call (pass a readonly struct with in to avoid the copy (in passes the struct without copying it); with a struct that isn't readonly, in passes a reference, but .NET makes a hidden copy each time you call one of its methods), and boxing makes a hidden heap copy, for example when a struct is used through an interface or an object parameter.
  • I don't change types on a hunch. I measure with BenchmarkDotNet (a tool that times code) and its memory diagnoser, which reports the bytes allocated per call, before and after.
What I left behind
  • After the incident: a review rule that every struct is a readonly struct with no setters, and a test that applies a payment and reads the instalment back from the list.

One billing run, every idea

The BMW FS nightly billing job, in time order. Each step names where the data lives and what gets copied.

1/8Value type. The job starts: int billed = 0 is a on its stack.

The story

  1. Before

    A finance quote is a dealer's offer to a BMW FS customer: the car's price and the months to pay. In code it is a . A developer added a "What if 48 months?" button to the dealer portal's API. It took the quote the customer had been sent, made a second variable for the preview, and changed the term on the preview.

    1/4Before: the customer was sent a quote for 36 months. The code holds it in sent.

  2. Why it had to change

    is a class, which makes it a reference type: a variable holds only the address of the object (a ), not the quote itself. So = copied the address. An int is a value type: its variable holds the number itself, so = copies the number. Same =, two different results, and the compiler can't warn you, because both are perfectly legal code.

  3. How it works now

    Here is what .NET does with memory when an object is built and then changed. The stack is the method's own space for its variables; the heap is where puts objects. (52_000m means 52,000 as a decimal.)

    1/2new Quote(52_000m, 36) builds the quote on the heap and returns its address.

  4. What we gained

    Once you know which kind a type is, you can predict what = and a method call will do. The rest of the card shows where each kind lives, struct vs class, and what each costs in speed.

In the lending API

The three copies. Each marked line is one =. "See it" (section 1) shows what each one copied.

Only the key lines are shown; Deep walks the whole file.

Lending.sln · where this file sits
  • tests/Domain.UnitTests/
⋯ 8 lines in Deep⋯ 10 lines in Deep
CopyTests.cs · line 7 · 1 of 3Copies the number. newTerm is a second, separate 36.

The code, file by file

Three xUnit tests (a test tool for .NET) that prove what = copies, and the two types they use. Tap Next to walk the diagram with the code.

Lending.sln · where these 3 files sit
  • tests/Domain.UnitTests/
  • src/Domain/
    • Quotes/
    • Billing/
Diagram · step 1int term = 36; An int is a value type: the variable holds the number itself.
CopyTests.cs · line 1 · 1 of 14A test class. xUnit runs every method marked [Fact] and reports pass or fail. Each test below proves one kind of copy.

What they ask next

The follow-ups interviews really ask. Answer out loud, then tap to check.

Follow-up tree

Answer, then open the next level. That is the order an interviewer digs in.

Reporteda candidate was asked Intervieweran interviewer asks it

Production bugs, and how to tell them

Each one: what you see, why, the fix in code, and the line to say.

Production incident: Paid instalments stayed unpaid overnight

P2 · serious, has a workaroundUDC Financecame with a deployEnd-of-day payments batch (a .NET worker on Azure)

Composed, not a real event at UDC Finance. The mechanism is real C#, checked by a run on .NET 10: a struct read from a List of T into a variable is a copy, so changing that variable never changes the list. Times, counts, names, ticket and PR numbers are composed.

The story in 5 lines
  1. On Wed 7 Oct, UDC Finance's collections team saw 1,840 contracts go into arrears (fall behind on a payment) overnight, about ten times a normal day. Most of those customers had paid on time.
  2. The matches each bank payment to the instalment it pays and marks that instalment paid. Release 3.4 had changed from a class to a struct, after a review comment said structs are faster.
  3. The batch read each instalment from a list into a variable, set on it, and saved the list. With a struct, the variable was a copy, so the list, and the database, never saw the change.
  4. No error and no failing test: the batch logged "1,912 payments applied" as always. Arrears text reminders had gone to 1,840 customers at 07:00; the 09:00 letters were held.
  5. The fix: back to a class, because it has an identity and changes, and a rule that every struct is a readonly struct, so changing a copy can't compile.
Who's who (4 people)
You
.NET developer on the vendor team. Finds the cause, writes the fix.
Tech lead
Vendor senior. Reviews every change, runs the postmortem.
L2 pod
Vendor support. First look, runs the database queries, reruns batches.
UDC collections lead
Owns arrears letters and reminders. Decides what customers are told.

Tap a step. The map lights the place where it happens.

1/4 Detect

Detect: 1,840 contracts in arrears overnight, ten times normal. Customers say they paid.

Wed 7 Oct, 08:10 NZDT (00:40 IST)UDC collections lead, then the L2 pod

  • What: the collections lead's morning report shows 1,840 contracts newly in arrears (behind on a payment), against about 180 on a normal day. By 08:30 the call centre is taking calls: "I paid, and I got a text saying I'm behind."
  • Why it hid: the batch finished on time and logged "1,912 payments applied", the usual number. The money was in UDC's bank account; only the instalment rows were wrong.
  • How we saw it: compared yesterday's bank payments with the instalments marked paid. 1,912 payments, 0 instalments marked paid, and 0 every night since release 3.4 went out on Tuesday evening.
Azure SQL: payments vs instalments marked paid
SELECT
  (SELECT COUNT(*) FROM BankPayments
   WHERE ReceivedOn = '2026-10-06') AS Paid,
  (SELECT COUNT(*) FROM Instalments
   WHERE PaidOn = '2026-10-06') AS Marked;

-- Paid: 1912   Marked: 0
In theory

A nightly check that payments received equal instalments marked paid, with an alert when they differ.

In practice

The batch counted the payments it read, not the rows it changed, so its log looked normal.

Try it yourself · 10 minutes, on your Mac
  1. dotnet new console -n StructLab. Add public struct Instalment { public bool Paid { get; set; } }.
  2. Make a with two items. Loop with for, read each into a variable, set Paid = true on the variable.
  3. Print list[0].Paid: False. Change struct to class and run again: True.
  4. Change it to public readonly struct with public bool Paid { get; }: now due.Paid = true is a compile error (CS0200), so the bug can't be written.
You should see

A struct read from a list is a copy. Make structs readonly, and keep anything that changes a class.

The senior move

"A type with an identity that changes, like an instalment, is a class. A struct is only for a small value that never changes, and it is always readonly, so a lost change can't compile."

Sources Sourced: assigning a struct copies it; a list's indexer returns a copy, so changing it in place is error CS1612 (C# docs, "Structure types"; compiler error CS1612). Readonly structs refuse changes after construction ("readonly struct" in the same page); Checked with a real run on .NET 10. Composed: UDC Finance's systems, dates, counts, names, ticket and numbers, and the SQL.

Traps

Traps

What should you say instead? Tap to check.

Live coding

Easy first, then harder. Set a timer and type it before you look.

Task 1

Question index: Musteasy · 10 minutes

Predict what this program prints, line by line, before you run it. Then say, for each pair of lines, whether the variable held a value or a reference.

Question index: predict the output. Asked as "value vs reference types, with code examples" at SS&C Technologies
You are given

One file. is a class with a settable ; is a small demo struct with a settable .

Lending.sln · where this file sits
Stuck? Hints
  1. Which of the four types is a class, and which are value types?
  2. Does = copy the data or the address?
  3. Can a string ever be changed, or does + build a new one?
Show a solution
Lending.sln · where this file sits
Output (real run) · line 1 · 1 of 4b got its own copy of 36; changing b never touched a.
What the interviewer checks
  • 36: an int copy is a separate number.
  • 48: both variables point at one .
  • False: the struct copy is separate data.
  • UDC: + built a new string; s still points at the old one.
  • Can say which lines held values and which held references.

Task 2

Question index: Mustmedium · 15 minutes

Write two methods that take a : sets its term to 60, and points the parameter at a new 12-month quote. Predict what the caller sees after each. Then make work with ref, and say why returning a new quote is usually better.

Nvest Solutions: value types vs reference types
Stuck? Hints
  1. What exactly does the method get: the quote, or a copy of something?
  2. Changing the object vs changing the parameter: which one can the caller see?
  3. What does ref hand over instead of a copy?
Show a solution
Lending.sln · where these 2 files sit
  • Quotes/
QuoteChanges.cs · line 1 · 1 of 7A class holds methods you call without making an object. Program.cs calls them.
What the interviewer checks
  • : the caller sees 60 (same object).
  • : the caller still sees 36 (only the copy of the reference moved).
  • With ref: the caller sees 12.
  • Says a method that returns a new object is easier to read and test than one that rewrites the caller's variable.
With AI tools

Interviewers now ask how you check AI-written code. When an assistant suggests turning a class into a struct "for performance", I check three things: is it one small value that never changes (then a readonly struct), does any code change it after a copy (a list item, a variable, a property), and is it used through an interface or object, which boxes it. And I ask for a BenchmarkDotNet run with the memory diagnoser, not a claim.

Glossary: every name, one line each →

My notes

Paste the NOTES block from your doubt chat here. The grader never reads notes; the coach reads "Still unclear".

Read moreWhere it was asked