← Home
.NETOOP and C# basicsReported by candidates

Abstract class vs interface: what are the differences, and when do you choose each?

The second most-asked CS question in the index: 28 reports, from Infosys, Deloitte, Accenture, Wipro, HCLTech, Luxoft and FIS.

Say this first · about 30 seconds
Make it yours · UDC Finance deal checks (invented example)
The code, simplified

Invented to show the shape, in C#. Tap Next to walk one deal through both checks.

UDC.sln · where these 5 files sit
  • Checks/
Diagram · step 1 builds its list: our check and UDC's, both as .
IDealCheck.cs · line 3 · 1 of 1Declares the one method every check must have. It returns the problems found; an empty list means the deal passes.

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
  • An abstract class is a parent class that a family of classes inherits from, and you can't create it on its own. It can hold fields, a and shared methods, plus abstract methods, which have no body, so every child must write them. A class can inherit only one class.
  • An interface lists what a class can do: method and property names, with no fields and no constructor. Since C# 8 a method can have a default body. A class can implement many interfaces.
  • For example, at BMW FS, is abstract: every loan and lease has a balance and the same payment code, written once.
  • is an interface: a contract and an insurance policy can both go on the monthly bill, but a policy has no balance, so it is not a kind of contract.
  • How I choose: if each type is really a kind of the parent and they share data, an abstract class. If unrelated types only share something they can do, an interface. Often I use both: callers depend on the interface, and an abstract class shares the code behind it.
Check your answer: tick what you said

You said 0 of 8

Then add your example: UDC Finance deal checks (invented example)
  • Situation: at UDC Finance in New Zealand, every deal runs through checks before it goes live. Our team writes some checks; UDC's own developers write others.
  • What I did: talks to every check through one interface, . Our checks all share one rule (the term must be above 0), so I wrote it once in an abstract class, , and each of our checks adds only its own rule.
  • Result: UDC's implements directly, without our base class, and runs it the same way. A new check is one new class and one line in the list; the loop never changes.
  • What I'd say: the interface is the contract the caller depends on. The abstract class is shared code for our own checks; no one is forced to inherit it.

See it

1/9Before: billing loops a list of : loans and leases. Insurance has its own job and bill.

In plain words

The senior answer · about 2 minutes
What each one really is
  • An abstract class is an unfinished base class. It can have fields, a constructor and methods with real code, so it can keep its data valid for every child. For example, our constructor throws if the balance is negative, so no loan or lease can be built broken.
  • An interface only says what a class can do. It has no fields and no constructor, so it can't protect any data; each class that implements it decides how. For example, says "you have an amount due and a name to bill", and a contract and an insurance policy each answer in their own way.
  • The big practical difference is the count. A class gets exactly one base class, and that base brings its data and constructor along. It can implement as many interfaces as it needs. So I spend the base class on what a type is, and use interfaces for what it can do.
How I choose
  • First I ask: is each type really a kind of the parent, and can it keep every promise the parent makes? A lease is a contract: it has a balance and takes payments. If yes, and they share data, I use an abstract class.
  • If unrelated types only share an ability, I use an interface. An insurance policy goes on the same bill, but it has no balance to pay down. Making it a contract would mean a fake balance and a payment method that throws, so it implements instead.
  • If the only reason for a child class is one behaviour that changes, like a late-fee rule that differs per client, I don't subclass at all. The class holds that rule as an object behind an interface. That is .
  • And the code that uses these objects depends on the interface. The asks for a list of , so a test can hand it a fake in one line.
Default interface methods
  • Since C# 8, an interface method can have a body. That was added for one job: adding a member to an interface that many classes already implement, without breaking them. Without a body, every class that implements it stops compiling. Worse, a class in another team's DLL, the compiled file their project ships as, fails only when the app loads it.
  • It does not make an interface an abstract class. There is still no state and no constructor, and the default runs only when you call through the interface type. On a variable the method isn't even visible.
  • There is one trap I watch for. If the base class implements the interface but never writes that method, the default body is used for the whole family. A child that later writes a method with the same name does not replace it: calls through the interface still run the default. So I write the method in the base as , and children override it.
In production
  • Both together is the shape I use most. Callers and the see an interface; an abstract base shares the code behind it; and anyone can still implement the interface without our base.
  • For an interface that other teams build against, I treat every new member as a breaking change, meaning a change that can stop their code working. It gets a default body, and our build runs a check that compares the new version with the last release. We added that after a Navistar billing job failed to start (see the incident).

One lease, both tools at work

A lease is loaded, billed and paid. Watch which part of the work the abstract class does, and which the interface does.

1/7Abstract class: EF Core builds a Lease. Contract's constructor runs first and checks the balance.

The story

  1. Before

    BMW FS bills loans and leases through the abstract . Its insurance partner's car policies came on a separate bill, and customers asked for one. Here are the two tempting fixes, and why both go wrong.

    1/6Fix 1: make inherit . Then billing's list of would take it.

  2. Why it had to change

    A base class is a one-time choice: a class gets exactly one, and it brings all its data and its constructor along. Spending it on an ability like "can be billed" either drags fake data into types that don't have any, or leaves no room for the next ability.

  3. How it works now

    With an interface, the billing loop is foreach (IBillable b in due) b.AmountDue();. Here is how .NET finds the right code for each object while the app runs, including a default body that no class wrote (the for classes is on its page):

    1/5At build time the compiler sees only IBillable b. It has , so the line builds.

  4. What we gained

    One bill for both families, with no fake balance on a policy. The next ability is one more interface, implemented only by the classes that have it. And a billing test can pass in any tiny test class that implements .

In the lending API

The contract family's base. It is abstract and implements ; the comments mark what only an abstract class can do.

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

Lending.sln · where this file sits
  • src/Domain/Contracts/
⋯ 5 lines in Deep⋯ 3 lines in Deep⋯ 10 lines in Deep
Contract.cs · line 3 · 1 of 4: nobody can write new Contract(...); every real one is a Loan or a Lease. : IBillable implements the interface, so every child implements it too.

The code, file by file

The billing types in the lending API: one interface, two abstract families, and the that bills them together. Tap Next to walk the diagram with the code.

Lending.sln · where these 6 files sit
  • src/
    • Domain/
      • Billing/
      • Contracts/
      • Insurance/
    • Application/Billing/
Diagram · step 4An lists only what a class can do. No data, no constructor.
IBillable.cs · line 4 · 1 of 5An interface: names only. No fields, no constructor. You can't new IBillable(), but a variable can have this type and hold any object that implemented it.

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: Truck-lease bills not sent: a new interface member broke another team's add-on

P2 · serious, has a workaroundNavistar Financialcame with a deployMonthly billing job in the servicing API, with Navistar's truck-lease add-on

Composed, not a real event at Navistar Financial. The mechanism is real .NET: when an interface gains a member with no body, a class compiled against the old version fails with TypeLoadException the moment the app loads it. The names, counts and times are composed.

The story in 5 lines
  1. On 1 Nov the monthly billing job stopped at start-up, and no Navistar Financial truck-lease bills were made.
  2. The error was TypeLoadException, which .NET throws when it can't load a class: the add-on's "does not have an implementation" of .
  3. Our shared billing package (a library we publish for other teams to build on) went to version 2.0, which added string Summary(); to with no body. We updated our own classes; the add-on, a DLL (the compiled file of a project) built by another team, was still built against 1.4.
  4. Fix: give a default body, so a class that never heard of it still loads. Release 2.0.1 the same morning; the old add-on loaded unchanged.
  5. Prevent: the package build now compares each new version with the last release and fails if old code would break; and a new interface member must come with a default body.
Who's who (5 people)
You
.NET developer on the vendor team. Owns the shared billing package; finds the cause, writes the fix.
Tech lead
Vendor senior. Reviews every change, runs the postmortem.
L2 pod
Vendor support. First look, reads the alerts and logs.
Add-on team
Another vendor team. Builds Navistar's truck-lease add-on as its own DLL against our package.
Navistar billing lead
Owns customer bills. Decides when late bills go out.

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

1/4 Detect

Detect: the job dies at start-up, before billing anything. Alert: TypeLoadException.

Sat 1 Nov, 00:30 CT (11:00 IST)Azure Monitor alert, then the L2 pod

  • What: the alert "billing job failed at start" fires 2 minutes after the job's scheduled run. No Navistar truck-lease bills exist for November.
  • Why it showed fast: the failure is loud. The runtime throws the first time it loads the add-on's class, before a single bill is made.
  • How we saw it: runs the query below. One exception, from one type, in the add-on's DLL.
App Insights: exceptions in the billing job
exceptions
| where timestamp > ago(1h)
| where cloud_RoleName == "billing-job"
| summarize count() by type,
    outerMessage
// System.TypeLoadException · 1
// Method 'Summary' in type
// 'Navistar.AddOn.TruckLeaseBill'
// does not have an implementation.
In theory

A release of the shared package triggers a rebuild and test of every add-on that uses it, before the monthly run.

In practice

The add-on lives in another team's repo, and nobody linked the two. We found out at the monthly run.

Try it yourself · 15 minutes, on your Mac
  1. dotnet new classlib -n Billing. Write public interface IBillable { decimal AmountDue(); }.
  2. dotnet new classlib -n AddOn, reference Billing, write public class Truck : IBillable { public decimal AmountDue() => 1m; } and build it.
  3. dotnet new console -n Job, reference both projects, and create a with Activator.CreateInstance(typeof(Truck)). Run it: fine.
  4. Add string Summary(); to and build only Billing. Copy Billing/bin/Debug/net8.0/Billing.dll over the one in Job/bin/Debug/net8.0/, then run the built Job directly (dotnet Job/bin/Debug/net8.0/Job.dll, not dotnet run, which would rebuild everything): TypeLoadException.
  5. Give a body (=> "";), build Billing, copy the DLL again, run the same way: it loads.
You should see

Adding a member with no body to an interface breaks classes built before it, even ones you never compile. A default body keeps them working.

The senior move

"Once another team builds on our interface, I treat every new member as a breaking change: it gets a default body, and the package build checks old code still fits."

Sources Sourced: TypeLoadException for a missing interface member, and default interface methods as the way to add members safely (C# docs, "Update interfaces with default interface methods"); package validation (.NET docs). Composed: Navistar Financial's systems, the add-on, dates, counts, names, ticket and numbers, and the (standard columns).

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 · 20 minutes

Put loans and car insurance policies on one monthly bill. stays the abstract base for loans and leases; already inherits . Design the types and a billing method, and prove it with a test.

Question index (Must, easy): UST asks for a scenario choosing an abstract class over an interface
Stuck? Hints
  1. Can have a second base class?
  2. What do a loan and a policy really have in common: data, or something they can do?
  3. What type should the billing method's list have?
Show a solution
Lending.sln · where this file sits
  • tests/Application.Tests/
BillingTests.cs · line 7 · 1 of 3Two families in one list, typed by the interface they both implement.
What the interviewer checks
  • An interface with and , implemented by both and .
  • No fake balance on a policy, and no on it.
  • The billing method takes , with no if on the type.
  • decimal for money.
  • You can say why stays abstract: shared data and the balance check.
  • Senior extra: a tiny test class that implements proves the billing method needs nothing else.

Task 2

Reportedmedium · 15 minutes

What does this program do? For each of the three marked lines, say whether it compiles and, if it does, what it prints. Then make lease.Describe() work.

Bizsense: two interfaces with the same default method
You are given
Lending.sln · where this file sits
Stuck? Hints
  1. Does a class get an interface's default body as its own method?
  2. Lines 2 and 3: which variable type picks the body?
  3. What happens to both interfaces if writes its own ?
Show a solution
Lending.sln · where these 2 files sit
Diagram · step 1Line 1: has no of its own. The defaults belong to the interfaces. CS1061.
Program.cs · line 2 · 1 of 3A class doesn't inherit a default body as its own member. Remove this line to run the rest.
What the interviewer checks
  • Line 1 does not compile (CS1061): has no of its own.
  • With line 1 removed: line 2 prints "statement", line 3 prints "audit".
  • Two same-named defaults are not a conflict: they are separate methods.
  • Fix: writes public string Describe(); then all three lines print its text.
  • Senior extra: name when it does fail (CS8705: both override one parent interface's method).
With AI tools

Copilot and other assistants tend to write an interface for every class ( with one version and no test fake) and a new abstract base whenever two classes share a few lines, because both are common in the code they learned from. In review I ask two questions: does anything really swap this interface, and is each child truly a kind of its abstract base? I also check any new member on a shared interface: it needs a default body, or every other team's build breaks.

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