Last Updated: May 17, 2026
Abstract classes and interfaces look similar on a slide. Both declare members without fully implementing them, both can be used as a base type for polymorphism, and both refuse to let you write new on them directly. The differences are about what each one is actually for: an abstract class is a partially built base type, an interface is a contract. This lesson turns the question "should this be an abstract class or an interface?" into a decision you can make in seconds.
A quick recap.
An abstract class is a class you can't instantiate. It can mix concrete members (fields, constructors, regular methods, properties) with members that derived classes must complete (abstract methods and properties). It participates in single inheritance: a derived class picks exactly one base. Abstract classes carry state and identity, so two abstract base types can't both be parents of the same derived class.
An interface is a contract. It lists members a type promises to provide. Until C# 8, interfaces had no implementation and no fields at all. From C# 8 onward, interfaces can carry default implementations and even static members, which blurs the line a bit. But the core idea hasn't changed: an interface describes capability, not lineage. A class can implement many interfaces, and unrelated types can implement the same interface without sharing any common ancestor.
The cleanest way to feel the difference is to look at the same idea expressed both ways. Here's a Discount modeled as an abstract class:
The base class stores Code, runs a constructor to set it, and ships a protected LogApplied helper. The derived class focuses on the rule that's unique to it. That kind of "give me half the implementation for free" is exactly what abstract classes are for.
Now the same idea as an interface:
The interface version is shorter, but every implementer is on its own for storing Code, validating it, and providing helpers. Every class that implements IDiscount re-derives the same plumbing. That's the trade: interfaces buy you flexibility and lose you free implementation.
The decision usually boils down to a handful of properties. This table lists every one that matters in practice, with the modern-C# caveats called out where they apply.
| Feature | Abstract Class | Interface |
|---|---|---|
| Instance state (fields) | Yes. Can declare instance fields and use them in concrete methods. | No instance fields. Static fields allowed since C# 8. |
| Constructors | Yes. Protected constructors run when a derived class is instantiated. | No constructors. Interfaces don't construct anything. |
| Auto-property backing fields | Yes. public string Code { get; set; } allocates real storage. | Auto-properties are abstract by default. Implementer provides storage. |
| Access modifiers on members | Full set: public, protected, internal, private, private protected. | Members were public-only before C# 8. Since C# 8, members can be public, private, protected, internal, static, virtual, sealed, with the caveat that non-public members need a default body. |
abstract members | Yes. Marked abstract, force overrides in non-abstract subclasses. | All members were implicitly abstract pre-C# 8. From C# 8, members are abstract unless they have a default body. |
| Default implementations | Yes. Mix abstract and concrete freely. | Yes from C# 8 onward (Default Interface Methods). |
| Static members | Yes, including static fields, methods, and constructors. | Yes for static methods and fields. Static abstract members since C# 11 (used in generic math and similar patterns). |
| Single vs multiple parents | One base class only. Single inheritance. | A type can implement many interfaces. |
| Instantiation | No. new Discount() is a compile error if Discount is abstract. | No. new IDiscount() is a compile error. Use a class that implements it. |
| Adding a new member later (evolution) | Adding a concrete member is non-breaking. Adding an abstract member breaks all derived classes. | Adding a member without a default body breaks every implementer. Adding a member with a default body is non-breaking from C# 8 onward. |
| Constructors in derived types | Derived constructors must chain to a base constructor (implicit or explicit : base(...)). | Implementers have their own constructors with no chaining required. |
sealed on members | Allowed (e.g., sealed override). | Member sealing through sealed was added in C# 8 for default methods. |
| Conveys identity ("is-a") | Yes. A PercentOff is a Discount. | Conveys capability ("can do"). An Order can be IPrintable. |
| Multiple implementations on the same class | A class has exactly one Discount base. | A class can implement IDiscount, IComparable<T>, IFormattable all at once. |
| Mocking and testing | Mockable, but mocks must subclass and override virtual members. | Easy. Test doubles implement the interface directly. |
| Dependency injection contract | Workable, but typical C# DI containers register against interfaces by convention. | Idiomatic. services.AddSingleton<IDiscount, PercentOff>(). |
| Cross-language interop | Generally fine within .NET. | The standard contract surface for cross-assembly APIs. |
| BCL example | Stream, HttpContent, DbConnection. | IEnumerable<T>, IDisposable, IComparable<T>. |
A few rows are worth unpacking because they trip people up:
private, protected, and internal members, but only when those members have a default body. The compiler enforces this: a non-public member must be usable somehow, and without a default body it would be both abstract and inaccessible to implementers.INumber<T>) and similar patterns where you want an interface to express things like "every implementing type has a static Zero property." For everyday application code, you probably won't write static abstract members, but they show up in modern BCL APIs.void Refund() as an abstract member to an existing abstract class breaks every derived class that didn't already implement Refund. Adding the same member to an interface, with a default body, breaks nothing. That's the whole reason Default Interface Methods were added: API evolution without breakage.Reach for an abstract class when the type you're modeling has substance beyond a contract. Substance means data, lifecycle, shared concrete behavior, or a real "is-a" relationship that derived classes can't sensibly opt out of.
Concrete reasons:
Order base could store OrderId, CustomerId, Status, and a list of items. Interfaces can't carry instance fields. Trying to express shared state through an interface forces every implementer to repeat the same five properties.LogApplied from the earlier Discount example is one. A shared MarkAsShipped() method on an abstract Shipment is another.Stream is the classic example: it owns position, dispose state, async machinery. Subclasses just plug in the actual byte-shoveling.Stream s carries semantics: it has a position, it might be writable, it can be disposed. IDisposable s is just "this can be disposed."The Template Method pattern is worth seeing directly because it leans on the abstract-class strengths in one place.
Three things make this design only work cleanly on an abstract class: the constructor that captures CustomerName, the protected hooks that aren't part of any public contract, and the inherited SendReceipt that subclasses get for free. Rewriting it as an interface would force CustomerName to be re-declared everywhere, lose the protected helpers, and trade the template method for a default interface method that can't reach private state.
Reach for an interface when the type you're modeling is fundamentally a capability or a contract, not a lineage.
Concrete reasons:
Product might be IComparable<Product> (so you can sort it), IFormattable (so you can format it), and IEquatable<Product> (so equality works correctly). None of those implies a "kind of product" relationship.IDisposable says "this object owns something that needs releasing." IEnumerable<T> says "you can iterate this." Code that takes IDisposable doesn't care whether you're a file handle or a database connection.Order and Invoice might both implement IPrintable even though one extends Document and the other extends Transaction. An abstract base could not have lived in both hierarchies.IEnumerable<T>, IDictionary<K, V>, ILogger, IConfiguration. Consumers depend on the interface, the library is free to ship any implementation.services.AddScoped<IDiscount, PercentOff>(). Consumers ask for IDiscount in their constructor and the container hands them whatever was registered. This decoupling is the whole point.A small DI example shows the everyday shape:
Cart doesn't care where inventory or prices come from. In a test, you could pass in a stub IInventory that always returns true and a stub IPriceList that returns whatever you want. There's no constructor chain to deal with, no protected helpers to worry about, just contract satisfaction.
Default Interface Methods (C# 8) and static abstract members (C# 11) made interfaces more powerful, which made the old "interface vs abstract class" advice partially out of date. It's worth knowing where the line still matters and where it's mostly stylistic.
Where the line still genuinely matters:
Where the line is largely stylistic now:
virtual plus default body, interface with default interface method.The honest framing: abstract class still wins when you need state, constructors, or strong identity. Interface still wins when you need multiple inheritance, lightweight contracts, or capability tagging. The "default implementation" tiebreaker that used to swing decisions toward abstract classes is gone. That's the only major shift.
The diagram splits modern capabilities into two columns. The top three (default implementation, static members, member visibility) used to be abstract-class-only. They aren't anymore. The bottom three (instance fields, constructors, single base identity) are still abstract-class-only, and that's where the practical decision actually lives.
The Base Class Library follows these rules consistently, which makes it a good teacher.
System.IO.Stream is an abstract class. It owns state (Position, CanRead, CanWrite, CanSeek), it has a lifecycle (open, read/write, dispose), it provides shared concrete methods on top of a small core of abstract methods (Read, Write, Seek, SetLength, Flush). Subclasses like FileStream, MemoryStream, and NetworkStream plug in the byte-shoveling and inherit everything else. None of that could work as an interface, because the lifecycle and state need a constructor and instance fields.
System.Collections.Generic.IEnumerable<T> is an interface. It says one thing: "you can ask me for an enumerator." Implementers range from arrays to lists to custom iterator methods generated by yield. They share no base class. Iterating a List<T>, an array, a dictionary's keys, and the output of a LINQ query all go through the same IEnumerable<T> contract even though those types have nothing else in common.
System.IDisposable is an interface. It says "you own something that needs releasing." Hundreds of unrelated types implement it: FileStream, HttpClient, SqlConnection, CancellationTokenSource. The interface gives them a common shape for using blocks to target without claiming they're all "kinds of" anything.
System.IO.TextReader and System.IO.TextWriter are abstract classes. Like Stream, they carry state, have lifecycle, and ship shared concrete implementations on top of a small abstract core. Subclasses like StreamReader and StringWriter plug in the source-or-destination details.
System.IComparable<T> is an interface. It's pure capability: "I can be compared to others of my type." Any type can declare it independently of any base class.
The pattern: anything with state, lifecycle, or shared concrete behavior is an abstract class. Anything that's a pure contract or capability is an interface. The BCL almost never mixes the two for the same role.
A few mistakes show up regularly when people are choosing between the two.
Putting access modifiers on interface members the wrong way. Before C# 8, this code was an error:
Pre-C# 8, all interface members were implicitly public and abstract. Writing public abstract on an interface member produced CS0106: The modifier 'public' is not valid for this item. In C# 8 and later, the same line compiles, because access modifiers on interface members are now allowed. If you see this error in a project, check the target language version (<LangVersion> in the csproj) before assuming the code is wrong.
The corrected pre-C# 8 form is simpler:
Treating "I might add a default later" as a reason to pick abstract class. Until C# 8, this was a real argument. From C# 8 forward, interfaces can ship defaults too. The decision should be based on whether you need state, constructors, and identity, not on whether you need defaults.
Using an abstract class with no abstract members. If every member of the abstract class has a default implementation and no derived class is forced to provide anything, the type might as well be a regular (possibly sealed) class with an interface alongside it. Marking a class abstract should mean "this is genuinely incomplete; subclasses must complete it."
Forcing an "is-a" relationship through an interface. Interfaces describe what a type can do, not what it is. Naming an interface IProduct and treating it as a stand-in for "is a kind of product" gets the modeling backwards. A product probably wants to be an abstract class (or a record class) that carries state. The interface is for capabilities the product participates in: IComparable<Product>, IPriceable, and so on.
Adding a new abstract method to a shipped abstract class. This silently breaks every consumer. If you absolutely must do it, either ship a default body via virtual, or use a new abstract subclass that consumers can opt into. The same warning applies to adding non-default methods to a shipped interface: it breaks every implementer.
Implementing a Default Interface Method and then expecting it to be reachable through the implementing class. Default interface methods are dispatched through the interface, not through the class. The following surprises people:
The default Log lives on the interface. The class PercentOff doesn't inherit it as an instance method visible on PercentOff. You have to call it through an IDiscount reference. This is unlike abstract classes, where inherited methods are visible directly on the derived class.
The two tools aren't mutually exclusive. A common, useful pattern is an abstract base class that implements an interface. The abstract class provides shared state and concrete behavior; the interface provides the contract that the outside world depends on.
The benefits stack up:
Code storage, constructor validation, and the LogApplied helper. Those live on the abstract class.stack list, dependency injection containers, test code) depends on IDiscount. The abstract class is an implementation detail.IDiscount directly without subclassing DiscountBase.This pattern shows up all over the BCL. Stream implements IDisposable. List<T> implements IList<T>, ICollection<T>, IEnumerable<T>, and several others. The abstract or concrete base provides the implementation; the interface lets unrelated types participate in the same contract.
When you have to pick, this checklist gets you to an answer fast.
The flowchart starts from the most decisive question (do you need instance state?) and walks downward. Each step eliminates one tool or the other. The default at the bottom is "interface, with an optional abstract base if a partial implementation would help." That default reflects modern C# practice: lean toward interfaces for public contracts, and add an abstract base alongside when subclasses would otherwise duplicate work.
A worked example shows the flowchart in action. Suppose you're modeling a PaymentMethod.
Now a different example: a IFormattable-style IPrintable that lets any object render itself to a string.
IPrintable and IEmailable and IComparable<Order>. The chart stops and says interface.A mixed example: Discount from earlier.
Code). Abstract class, but also worth implementing IDiscount so consumers depend on the contract, not the base class.That last example is the "abstract base implements interface" pattern shown earlier in this lesson.
A condensed lookup table for when you remember the comparison generally but can't recall a specific cell:
| Need | Abstract Class | Interface |
|---|---|---|
| Share fields across subclasses | Yes | No |
| Run code in a constructor | Yes | No |
| Force derived types to implement methods | abstract members | All abstract by default |
| Provide a default body | Yes | Yes (C# 8+) |
| Mix concrete and abstract members | Yes | Yes (C# 8+) |
| Inherit from multiple parents | No, single base | Yes, many interfaces |
| Express "is a kind of X" | Yes | No, use abstract class |
| Express "can do X" | Awkward | Yes |
| Standard DI / mocking target | Workable | Idiomatic |
| Add a new member to a shipped library safely | virtual with default body | Default interface method |
| Be the public API surface for a library | Sometimes | Usually |
The two tools answer different questions. Once you know which question you're asking, the choice falls out.
Stream, TextReader, DbConnection, and HttpContent are abstract classes because they own state and lifecycle. IEnumerable<T>, IDisposable, IComparable<T>, and IFormattable are interfaces because they express pure capability.