AlgoMaster Logo

Abstract Class vs Interface

Last Updated: May 17, 2026

14 min read

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.

What Each One Actually Is

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 Comparison Table

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.

FeatureAbstract ClassInterface
Instance state (fields)Yes. Can declare instance fields and use them in concrete methods.No instance fields. Static fields allowed since C# 8.
ConstructorsYes. Protected constructors run when a derived class is instantiated.No constructors. Interfaces don't construct anything.
Auto-property backing fieldsYes. public string Code { get; set; } allocates real storage.Auto-properties are abstract by default. Implementer provides storage.
Access modifiers on membersFull 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 membersYes. 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 implementationsYes. Mix abstract and concrete freely.Yes from C# 8 onward (Default Interface Methods).
Static membersYes, 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 parentsOne base class only. Single inheritance.A type can implement many interfaces.
InstantiationNo. 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 typesDerived constructors must chain to a base constructor (implicit or explicit : base(...)).Implementers have their own constructors with no chaining required.
sealed on membersAllowed (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 classA class has exactly one Discount base.A class can implement IDiscount, IComparable<T>, IFormattable all at once.
Mocking and testingMockable, but mocks must subclass and override virtual members.Easy. Test doubles implement the interface directly.
Dependency injection contractWorkable, but typical C# DI containers register against interfaces by convention.Idiomatic. services.AddSingleton<IDiscount, PercentOff>().
Cross-language interopGenerally fine within .NET.The standard contract surface for cross-assembly APIs.
BCL exampleStream, HttpContent, DbConnection.IEnumerable<T>, IDisposable, IComparable<T>.

A few rows are worth unpacking because they trip people up:

  • Access modifiers on interfaces (C# 8). Interface members were strictly public before C# 8. Modern interfaces can declare 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.
  • Static abstract members (C# 11). This one is niche. It exists for generic math (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.
  • Evolution rules. This is the single biggest practical difference for library authors. Adding 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.

When To Choose an Abstract Class

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:

  1. Shared instance state. The base class holds fields that every subclass needs to read or write. An 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.
  2. Shared concrete behavior. When several subclasses need an identical helper method, putting it on an abstract base avoids copy-paste. LogApplied from the earlier Discount example is one. A shared MarkAsShipped() method on an abstract Shipment is another.
  3. Common protected helpers. Protected members are an abstract-class superpower. They let a base class hand subclasses "internal" tools that callers can't see. Interfaces from C# 8 can declare protected members, but only those with default bodies, and the visibility rules are different (protected interface members aren't accessible the same way protected class members are).
  4. Partial implementation. You have a template where most of the algorithm is fixed but a few steps vary. The Template Method pattern fits naturally on an abstract class: the base method orchestrates, an abstract or virtual hook lets the subclass fill in the variable step.
  5. Lifecycle management. When the base type owns resources or state transitions, an abstract class is the right home. Stream is the classic example: it owns position, dispose state, async machinery. Subclasses just plug in the actual byte-shoveling.
  6. You want a real type, not just a contract. When code holds a reference of the base type, you want it to "be" something meaningful. 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.

When To Choose an Interface

Reach for an interface when the type you're modeling is fundamentally a capability or a contract, not a lineage.

Concrete reasons:

  1. Multiple inheritance of capability. A class can implement many interfaces. 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.
  2. Capability tagging. Some types exist to mark that a class supports something. 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.
  3. Contract for unrelated types. Two classes that share no parent can still implement the same interface. 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.
  4. Public API surface. Libraries usually expose interfaces, not abstract classes, as their primary contracts. IEnumerable<T>, IDictionary<K, V>, ILogger, IConfiguration. Consumers depend on the interface, the library is free to ship any implementation.
  5. Mocking and testing. Test doubles for interfaces are trivial: implement the interface, no inheritance required. Mocking libraries like Moq lean heavily on interface targets. Mocking an abstract class works too, but requires the mock to deal with constructors, sealed members, and base-class state.
  6. Dependency injection. Idiomatic C# DI registers interfaces against concrete implementations: 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.

Where Modern C# Blurs the Line

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:

  • Instance state. Interfaces still can't declare instance fields. If the base type needs to remember anything per-instance, abstract class wins. A default interface method can compute things, but it can't store them without delegating to abstract properties the implementer fills in.
  • Constructors. Interfaces still have no constructors. If construction must enforce invariants (validation, dependency capture, immutable setup), an abstract base with a protected constructor handles it cleanly.
  • Single base type identity. A class still picks exactly one base class. If you want the type to "be" something specific in the type system, that's an abstract class job.
  • Protected internal helpers. Protected members on abstract classes work the way most people expect. Protected interface members have a more complex visibility story and aren't the same tool.

Where the line is largely stylistic now:

  • Providing a default implementation. Pre-C# 8, only abstract classes could do this. Now both can. The remaining differences are about how the default is dispatched and whether it can access state.
  • Adding members without breaking consumers. Both can do this now: abstract class with virtual plus default body, interface with default interface method.
  • Static methods. Both support them.

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.

Real-World Patterns in the BCL

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.

Common Mistakes

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.

Mixing Both: Abstract Base That Implements an Interface

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:

  • Subclasses share Code storage, constructor validation, and the LogApplied helper. Those live on the abstract class.
  • The outside world (the stack list, dependency injection containers, test code) depends on IDiscount. The abstract class is an implementation detail.
  • Adding a new discount type means writing one subclass. The abstract base does the boilerplate.
  • A future discount that can't fit the abstract base's shape (say, an entirely different lineage) can still implement 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.

The Decision Flowchart

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.

  • Do implementers share instance state? Yes (transaction id, customer id, amount). The chart stops here and says abstract class.

Now a different example: a IFormattable-style IPrintable that lets any object render itself to a string.

  • Do implementers share instance state? No, the data lives on the implementer.
  • Do they share a constructor? No, the implementer constructs itself.
  • Should one class satisfy multiple contracts? Yes, an order might be IPrintable and IEmailable and IComparable<Order>. The chart stops and says interface.

A mixed example: Discount from earlier.

  • Shared state? Yes (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.

What Each One Buys You at a Glance

A condensed lookup table for when you remember the comparison generally but can't recall a specific cell:

NeedAbstract ClassInterface
Share fields across subclassesYesNo
Run code in a constructorYesNo
Force derived types to implement methodsabstract membersAll abstract by default
Provide a default bodyYesYes (C# 8+)
Mix concrete and abstract membersYesYes (C# 8+)
Inherit from multiple parentsNo, single baseYes, many interfaces
Express "is a kind of X"YesNo, use abstract class
Express "can do X"AwkwardYes
Standard DI / mocking targetWorkableIdiomatic
Add a new member to a shipped library safelyvirtual with default bodyDefault interface method
Be the public API surface for a librarySometimesUsually

The two tools answer different questions. Once you know which question you're asking, the choice falls out.

Summary

  • Abstract classes are partially built base types. They carry state, run constructors, mix concrete and abstract members, and express identity. Each class has exactly one abstract base.
  • Interfaces are contracts. They describe capability, not lineage. A class can implement many interfaces, including across unrelated parts of the type system.
  • Modern C# (8+) gives interfaces default implementations and (in C# 11) static abstract members. The line between the two narrowed, but state, constructors, and single identity still belong to abstract classes only.
  • Choose abstract class when implementers share state, share constructor logic, share protected helpers, or fit a Template Method shape. Choose interface for capability tagging, multiple inheritance of contract, public API surface, and DI / mocking.
  • The most useful pattern in practice is "abstract base implements interface": the base provides storage and helpers, the interface is the public contract consumers depend on.
  • Evolution is now symmetric. Adding a member to a shipped abstract class is non-breaking when it's virtual with a default body. Adding a member to a shipped interface is non-breaking when it's a default interface method.
  • BCL examples set the model. 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.