Ask this in an interview and you get a memorised list: “an interface has no implementation, an abstract class can have one, and you can only inherit one class”. All true, all useless, because it does not tell you which to reach for on a Tuesday afternoon. This post gives the mechanics properly and then the decision rule I actually use. It builds on generics and extension methods, and sets up inheritance and polymorphism next.
Two different questions
The confusion comes from treating these as competitors. They answer different questions:
- An interface answers “what can this thing do?” It is a contract. Any type, from any part of the hierarchy, can sign it.
- An abstract class answers “what is this thing, and what does it already have?” It is a partially built implementation with holes for subclasses to fill.
“Can do” versus “is a”. Hold on to that; everything below is detail.
The interface
public interface IBookExporter
{
string FileExtension { get; }
string Export(IEnumerable<Book> books);
}
No fields, no constructor, no state, just members a type promises to provide. Anyone can implement it:
public class CsvExporter : IBookExporter
{
public string FileExtension => ".csv";
public string Export(IEnumerable<Book> books) =>
string.Join("\n", books.Select(b => $"{b.Id},{b.Title},{b.Price}"));
}
public class JsonExporter : IBookExporter
{
public string FileExtension => ".json";
public string Export(IEnumerable<Book> books) => JsonSerializer.Serialize(books);
}
And the code that uses them never names a concrete type:
public void Save(IBookExporter exporter, IEnumerable<Book> books, string path)
{
File.WriteAllText(path + exporter.FileExtension, exporter.Export(books));
}
Save works with every exporter that exists today and every one written next year, including a fake one in a unit test. That is the point of an interface: it is the seam where implementations can be swapped.
A type can implement any number of interfaces, and this is the ability that matters most:
public class ZipExporter : Stream, IBookExporter, IDisposable, IAsyncDisposable
ZipExporter already inherits Stream and still gets to be an exporter. That is impossible with an abstract class.
The abstract class
An abstract class is a class you cannot instantiate, because parts of it are missing.
public abstract class ExporterBase : IBookExporter
{
private readonly ILogger _logger; // state, interfaces cannot have this
protected ExporterBase(ILogger logger) => _logger = logger; // constructor, nor this
public abstract string FileExtension { get; } // hole: every subclass must fill it
protected abstract string Format(Book book); // hole, and protected, nor this
public string Export(IEnumerable<Book> books) // shared, finished behaviour
{
_logger.LogInformation("Exporting {Count} books", books.Count());
var sb = new StringBuilder();
foreach (var book in books)
sb.AppendLine(Format(book)); // calls down into the subclass
return sb.ToString();
}
}
public class CsvExporter : ExporterBase
{
public CsvExporter(ILogger logger) : base(logger) { }
public override string FileExtension => ".csv";
protected override string Format(Book b) => $"{b.Id},{b.Title},{b.Price}";
}
CsvExporter is now four lines. Everything shared (the logging, the loop, the buffer handling) lives once in the base class, and the subclass supplies only the part that genuinely differs. new ExporterBase(...) will not compile, because the class is by definition incomplete.
That Export method is a pattern worth naming: the template method. The base class owns the algorithm’s skeleton and delegates the varying steps downwards. It is the classic reason to choose an abstract class over an interface.
What each one can hold
| Interface | Abstract class | |
|---|---|---|
| Method signatures | yes | yes |
| Implemented methods | yes, as default implementations (C# 8+) | yes |
| Instance fields / state | no | yes |
| Constructor | no | yes (called by subclasses) |
protected members | no (all members are public) | yes |
| Access modifiers on members | no (implicitly public) | yes |
| Static members and constants | yes | yes |
| How many can a type have? | many | exactly one |
| Can be instantiated? | no | no |
| Versioning: adding a member | breaks implementers, unless it has a default body | safe, if it has a body |
Three rows carry the real weight: state, constructors, and how many. If your shared code needs a field, needs to run initialisation, or needs protected helpers, an interface cannot express it. If a type needs to play more than one role, an abstract class cannot express it.
Default interface methods do not change the answer
C# 8 let interfaces carry method bodies:
public interface IBookExporter
{
string FileExtension { get; }
string Export(IEnumerable<Book> books);
string ExportOne(Book book) => Export([book]); // default implementation
}
This looks like it collapses the distinction. It does not, and the reason is worth understanding.
Default implementations exist for versioning, not for code reuse. Before C# 8, adding a member to a published interface broke every implementer in every downstream project. A default body means old implementers keep compiling. That is the problem it was designed to solve.
They are still limited in the ways that matter: no instance state, so a default method can only use other interface members, and it is not inherited into the implementing class’s own API: call it through the interface reference or it is not visible. An abstract class is still the tool for sharing real implementation. Use default interface methods for convenience overloads and for adding to an interface you have already shipped.
The decision rule
In practice I ask three questions in order.
1. Am I defining a capability, or sharing an implementation? Capability → interface. Implementation → abstract class.
2. Will implementers already have a base class, or come from unrelated hierarchies? Yes → interface. An abstract class spends the one inheritance slot every class has, permanently, and taking it away from your users is a big ask.
3. Do I need shared state, a constructor, or protected members? Yes → abstract class.
And the answer that resolves most real cases is both:
public interface IBookExporter { ... } // the contract, for callers and tests
public abstract class ExporterBase : IBookExporter { ... } // optional shared plumbing, for implementers
Callers, DI registrations and mocks depend on the interface. Implementers may inherit the base class for convenience, or ignore it and implement the interface directly if they already have a parent. This is exactly what .NET does: IList<T> is the contract, Collection<T> is the optional base; Stream is abstract while IDisposable is the interface; ILogger is the contract and LoggerBase-style classes are just help.
Two more practical pushes toward interfaces in modern C#:
Dependency injection wants interfaces. services.AddScoped<IBookExporter, CsvExporter>() is the standard registration shape, and constructor parameters typed as interfaces are what make the whole container useful. (That is its own post later in this series.)
Tests want interfaces. Mocking libraries generate a stand-in for an interface trivially. Mocking an abstract class means every method you want to fake must be virtual, the constructor must be satisfiable, and any base-class side effects still run. If a dependency crosses a boundary (database, HTTP, filesystem, clock), put an interface on it.
Two things people get wrong
Interfaces are not free abstraction. Wrapping every single class in a one-implementation interface named IThing because “it is good practice” adds files, indirection and navigation cost with no seam anyone will ever use. Add an interface where you have a genuine reason: multiple implementations, a test boundary, or a package boundary you must not break.
Abstract classes attract too much. A base class that starts with a logger and a template method tends to grow, until subclasses inherit fifteen members and use three, and changing the base breaks everything below it. This is the fragile base class problem, and it is why the standard advice is “prefer composition over inheritance”. If a subclass only wants some of the base, inject a collaborator instead of inheriting it.
The same idea inside Java, Go and Swift
Java (Netflix, Android). Almost the same model, and it arrived at the same compromise: interfaces, single class inheritance, and default methods added in Java 8 for exactly the versioning reason C# added them in C# 8. Java’s Collection framework is the same interface-plus-abstract-base pairing: List is the interface, AbstractList is the optional helper.
Go (Google). No classes, no inheritance, and interfaces are implicit: a type satisfies an interface by having the right methods, without declaring anything:
type Exporter interface {
Export(books []Book) string
}
Any type with an Export method is an Exporter. This means the consumer defines the interface it needs, which is the ideal C# codebases approximate by hand: declare the interface where it is used, not next to the implementation. Go replaces the abstract class entirely with embedding, which is composition.
Swift (Apple). Protocols are interfaces, and protocol extensions give them default implementations, the C# 8 feature, years earlier and more capable. Swift’s community slogan is “protocol-oriented programming”: prefer many small protocols over class hierarchies. C# is drifting the same way.
Rust. Traits are interfaces with default methods, and there is no inheritance at all. Shared behaviour is composed from traits and generics. It is the strongest existing argument that everything an abstract class does can be done another way.
TypeScript (Meta, frontends). Has both, with the same rules, plus structural typing like Go: a matching shape satisfies an interface without declaring it.
The convergence across all of them is telling: every modern language keeps interfaces, and most either weaken or drop class inheritance. If you are unsure, that is the direction to lean.
Summary
- An interface is a contract: what a type can do. An abstract class is a partially built implementation: what a type is and already has.
- A type implements many interfaces and inherits exactly one class. That single inheritance slot is the scarcest resource in the design.
- Only an abstract class can hold instance state, a constructor, and
protectedmembers. Only an interface can be applied across unrelated hierarchies. - Default interface methods (C# 8) exist for versioning, not code reuse: they have no state and are not part of the implementing class’s public surface.
- The template method pattern, a fixed algorithm with subclass-supplied steps, is the canonical reason to pick an abstract class.
- The common best answer is both: an interface for callers, DI and tests, plus an optional abstract base for implementers.
- Prefer composition to deep inheritance, and do not add an interface until it buys you a real seam.
Interview questions and answers
1. What is the difference between an interface and an abstract class?
An interface declares a contract of capabilities with no instance state and no constructor, and a type may implement many. An abstract class is an incomplete class that can hold fields, constructors, protected members and implemented methods, and a type may inherit only one. Interfaces express “can do”; abstract classes express “is a, and here is some of it already”.
2. When would you choose an abstract class?
When implementations share real code and state: a logger field, initialisation logic that must run for every subclass, protected helpers, or a fixed algorithm with a few varying steps, the template method pattern. Also when the types genuinely form one family and you control them all.
3. When would you choose an interface?
When you are defining a capability that unrelated types may adopt, when implementers may already have a base class, when you need a seam for dependency injection or testing, or when several different things must be usable interchangeably by a caller.
4. Can an interface have implementation in C#?
Since C# 8, yes: default interface methods can have bodies. They exist so an interface can gain members without breaking existing implementers. They still cannot declare instance fields, and they are only callable through the interface, not through the implementing class, so they do not replace an abstract base class for sharing implementation.
5. Why does C# allow multiple interfaces but only one base class?
To avoid the diamond problem of multiple implementation inheritance: if two base classes provided different implementations of the same member, the runtime would have to choose. Interfaces historically carried no implementation, so there was nothing to conflict. With default interface methods the language now requires the implementing type to resolve any ambiguity explicitly.
6. Can an abstract class implement an interface?
Yes, and it is a common pattern. It may implement some members and leave others abstract for subclasses. That combination (interface for the contract, abstract class for shared plumbing) is how much of the .NET base class library is structured.
7. Can an abstract class have a constructor?
Yes. It cannot be called with new, but subclass constructors call it via base(...), which is how shared fields get initialised. This is one of the clearest capabilities an interface cannot match.
8. What is the fragile base class problem?
When many types inherit one base class, a change in that base (a new member, a changed call order, extra behaviour in a virtual method) can silently break subclasses that were written against the previous behaviour. It gets worse as the hierarchy deepens, and it is the main reason composition is generally preferred to inheritance.
9. Why do mocking frameworks prefer interfaces?
They generate a stand-in type at runtime. For an interface, every member is available to intercept. For a class, only virtual members can be overridden, the constructor must be satisfiable, and non-virtual base behaviour still executes. Interfaces make substitution complete and predictable.
10. Should every class have an interface?
No. An interface earns its place when there are multiple implementations, a boundary that must be faked in tests, or a published contract you cannot break. A one-to-one IThing/Thing pair with no second implementation and no test seam adds indirection without a benefit.