DEV Community

Chethan Ramaswamy
Chethan Ramaswamy

Posted on

Abstract Class vs Interface in C#: Demystifying the Difference

Abstract Class vs Interface in C#: Demystifying the Difference

Introduction

Few C# interview questions create as much confusion as:

"What is the difference between an abstract class and an interface,
and when should I use each?"

Many developers memorize a comparison table:

  • Abstract class → can have implementation
  • Interface → defines a contract
  • Abstract class → supports fields
  • Interface → does not
  • Abstract class → single inheritance
  • Interface → multiple inheritance

These statements are useful, but they do not answer the most important
question:

Why would I choose one over the other when designing a real
application?

This article builds the answer from first principles.

The simplest way to remember it is:

An interface describes what a type can do. An abstract class
describes what a family of related types fundamentally is, while
allowing shared behavior.

That distinction is much more valuable than memorizing syntax.


1. Start With the Real Design Problem

Suppose we are building an application that sends notifications.

We may have:

  • Email notification
  • SMS notification
  • Push notification

All of them can send a notification.

That immediately suggests a contract:

public interface INotificationSender
{
    Task SendAsync(string message);
}
Enter fullscreen mode Exit fullscreen mode

Now imagine that all notification providers also need:

  • Common logging
  • Common validation
  • A provider name
  • Shared retry behavior

That may suggest an abstract base class:

public abstract class NotificationSender
{
    protected readonly ILogger Logger;

    protected NotificationSender(ILogger logger)
    {
        Logger = logger;
    }

    public abstract Task SendAsync(string message);

    protected void LogSending(string message)
    {
        Logger.LogInformation("Sending notification: {Message}", message);
    }
}
Enter fullscreen mode Exit fullscreen mode

The two abstractions solve different design problems.

The interface says:

"Anything implementing me promises that it can send a notification."

The abstract class says:

"These objects belong to the same conceptual family and can share
state and implementation."

That is the core distinction.


2. What Is an Interface?

An interface is a contract.

It defines capabilities that a type promises to provide.

public interface IPaymentProcessor
{
    Task ProcessPaymentAsync(decimal amount);
}
Enter fullscreen mode Exit fullscreen mode

A class implements that contract:

public class StripePaymentProcessor : IPaymentProcessor
{
    public async Task ProcessPaymentAsync(decimal amount)
    {
        // Stripe-specific implementation
    }
}
Enter fullscreen mode Exit fullscreen mode

Another implementation can provide a completely different
implementation:

public class RazorpayPaymentProcessor : IPaymentProcessor
{
    public async Task ProcessPaymentAsync(decimal amount)
    {
        // Razorpay-specific implementation
    }
}
Enter fullscreen mode Exit fullscreen mode

The important point is:

The interface does not care how payment is processed.

It only defines the capability.


3. What Is an Abstract Class?

An abstract class is a base type that is incomplete by design.

It can contain:

  • Abstract methods
  • Concrete methods
  • Fields
  • Properties
  • Constructors
  • Protected members
  • Shared state
  • Static members

Example:

public abstract class PaymentProcessor
{
    protected readonly ILogger Logger;

    protected PaymentProcessor(ILogger logger)
    {
        Logger = logger;
    }

    public abstract Task ProcessPaymentAsync(decimal amount);

    protected void LogPayment(decimal amount)
    {
        Logger.LogInformation(
            "Processing payment of {Amount}",
            amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

A derived class must implement the abstract operation:

public class StripePaymentProcessor : PaymentProcessor
{
    public StripePaymentProcessor(ILogger logger)
        : base(logger)
    {
    }

    public override async Task ProcessPaymentAsync(decimal amount)
    {
        LogPayment(amount);

        // Stripe-specific implementation
    }
}
Enter fullscreen mode Exit fullscreen mode

Here the base class provides common behavior and state, while
subclasses provide specialized behavior.


4. The Most Important Difference

The easiest mental model is:

Interface = Capability

Ask:

"What can this object do?"

Examples:

IEnumerable
IDisposable
IComparable
ILogger
IPaymentProcessor
INotificationSender
Enter fullscreen mode Exit fullscreen mode

A class may support several unrelated capabilities.

For example:

public class OrderService :
    IOrderService,
    IDisposable,
    IAsyncDisposable
{
}
Enter fullscreen mode Exit fullscreen mode

These interfaces describe capabilities/contracts.


Abstract Class = Family / Common Foundation

Ask:

"What is this object fundamentally, and what behavior should all
members of this family share?"

For example:

PaymentProcessor
    ├── StripePaymentProcessor
    ├── RazorpayPaymentProcessor
    └── PayPalPaymentProcessor
Enter fullscreen mode Exit fullscreen mode

They share a common conceptual foundation.

Therefore an abstract class can make sense.


5. A Powerful Interview Rule

When deciding between them, ask two questions.

Question 1

Is this a capability that unrelated classes may need?

If yes, prefer an interface.

Example:

public interface IAuditable
{
    DateTime CreatedAt { get; }
}
Enter fullscreen mode Exit fullscreen mode

A customer, order, invoice, or product could all be auditable.

They do not need to belong to the same inheritance hierarchy.


Question 2

Do these classes belong to the same conceptual family and need
shared state or implementation?

If yes, an abstract class may be appropriate.

Example:

DocumentProcessor
    ├── PdfProcessor
    ├── WordProcessor
    └── ExcelProcessor
Enter fullscreen mode Exit fullscreen mode

All processors may share:

  • File validation
  • Logging
  • Configuration
  • Metrics
  • Common processing workflow

An abstract base class can encapsulate that common behavior.


6. Side-by-Side Comparison

Aspect Difference
Purpose Abstract class provides a common foundation. Interface defines a contract or capability.
Implementation Abstract class can provide shared implementation. Interface can also provide default implementations in modern C#.
State Abstract class can contain instance state. Interface cannot contain instance fields.
Constructors Abstract class can have constructors. Interfaces cannot have instance constructors.
Inheritance A class can inherit only one base class. A class can implement multiple interfaces.
Coupling Abstract classes create a stronger relationship. Interfaces generally provide looser coupling.
Best use Use for shared behavior, state, or workflow.
Typical example FileProcessor → PdfProcessor, ExcelProcessor
Interface example INotificationSender → EmailSender, SmsSender

7. Why Does C# Allow Multiple Interfaces but Only One Base Class?

Consider:

public class ReportService :
    IReportGenerator,
    IAuditable,
    IDisposable
{
}
Enter fullscreen mode Exit fullscreen mode

A class can implement multiple interfaces.

Why?

Because these interfaces represent independent capabilities:

ReportService
 ├── IReportGenerator
 ├── IAuditable
 └── IDisposable
Enter fullscreen mode Exit fullscreen mode

But C# allows only one class inheritance chain:

public class ReportService : BaseReportService
{
}
Enter fullscreen mode Exit fullscreen mode

The reason is that inheriting implementation and state creates much
stronger relationships.

If multiple base classes were allowed, problems such as conflicting
implementations and state become much harder to reason about.

So C# deliberately separates:

Capability composition

from

Implementation inheritance


8. Real Example: Interface Is Better

Imagine a logging system.

public interface INotificationSender
{
    Task SendAsync(string recipient, string message);
}
Enter fullscreen mode Exit fullscreen mode

Implementations:

public class EmailNotificationSender : INotificationSender
{
    public Task SendAsync(string recipient, string message)
    {
        // Send email
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode
public class SmsNotificationSender : INotificationSender
{
    public Task SendAsync(string recipient, string message)
    {
        // Send SMS
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

Why interface?

Because email and SMS are not necessarily related by inheritance.

They simply share a capability:

They can send notifications.

The interface keeps the design loosely coupled.


9. Real Example: Abstract Class Is Better

Consider file processors.

public abstract class FileProcessor
{
    public async Task ProcessAsync(string filePath)
    {
        Validate(filePath);

        await ReadAsync(filePath);

        Log(filePath);
    }

    protected void Validate(string filePath)
    {
        if (string.IsNullOrWhiteSpace(filePath))
            throw new ArgumentException("Invalid file path.");
    }

    protected abstract Task ReadAsync(string filePath);

    protected void Log(string filePath)
    {
        Console.WriteLine($"Processed {filePath}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Now:

public class PdfProcessor : FileProcessor
{
    protected override Task ReadAsync(string filePath)
    {
        // PDF-specific logic
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

And:

public class ExcelProcessor : FileProcessor
{
    protected override Task ReadAsync(string filePath)
    {
        // Excel-specific logic
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

The abstract class provides a common workflow:

Validate
   ↓
Read
   ↓
Log
Enter fullscreen mode Exit fullscreen mode

Only the specialized operation changes.

This is a classic use case for an abstract class.


10. Interface Does NOT Mean "No Implementation"

This is an important misconception.

Modern C# interfaces can contain default implementations.

For example:

public interface ILogger
{
    void Log(string message);

    void LogError(string message)
    {
        Log($"ERROR: {message}");
    }
}
Enter fullscreen mode Exit fullscreen mode

So this statement is outdated:

"Interfaces cannot contain implementation."

A better statement is:

Interfaces primarily define contracts, but modern C# allows them to
provide default implementations as well.

However, that does not mean interfaces should become replacements for
abstract classes.

Default interface implementations solve specific versioning and
API-evolution scenarios. They do not provide instance state or turn an
interface into a normal stateful base class.


11. Abstract Class vs Interface in Dependency Injection

This is where interfaces are extremely common in .NET applications.

Example:

public interface IOrderRepository
{
    Task GetByIdAsync(int id);
}
Enter fullscreen mode Exit fullscreen mode

Implementation:

public class SqlOrderRepository : IOrderRepository
{
    public Task GetByIdAsync(int id)
    {
        // Database implementation
        throw new NotImplementedException();
    }
}
Enter fullscreen mode Exit fullscreen mode

Register it:

builder.Services.AddScoped();
Enter fullscreen mode Exit fullscreen mode

The application depends on:

IOrderRepository
Enter fullscreen mode Exit fullscreen mode

instead of:

SqlOrderRepository
Enter fullscreen mode Exit fullscreen mode

This provides loose coupling.

It also makes testing easier:

public class FakeOrderRepository : IOrderRepository
{
    // Test implementation
}
Enter fullscreen mode Exit fullscreen mode

This is one reason interfaces are heavily used in enterprise .NET
applications.


12. Does That Mean We Should Always Use Interfaces?

No.

This is another common mistake.

Some developers create an interface for every single class:

CustomerService
ICustomerService

OrderService
IOrderService

ProductService
IProductService
Enter fullscreen mode Exit fullscreen mode

without any real architectural reason.

This can produce unnecessary abstraction.

For example, if a class:

  • Has one implementation
  • Is internal to a small module
  • Does not need polymorphism
  • Does not represent a meaningful boundary

then creating an interface may add complexity without providing much
value.

The goal is not:

"Use interfaces everywhere."

The goal is:

Use abstraction where it creates a useful boundary.


13. When Should You Use an Interface?

Prefer an interface when:

1. You need a contract

public interface IPaymentProcessor
{
    Task ProcessAsync(decimal amount);
}
Enter fullscreen mode Exit fullscreen mode

2. Multiple unrelated classes need the same capability

Order
Customer
Invoice
Product
Enter fullscreen mode Exit fullscreen mode

could all implement:

IAuditable
Enter fullscreen mode Exit fullscreen mode

3. You need multiple capabilities

public class Order :
    IAuditable,
    IValidatable,
    IExportable
{
}
Enter fullscreen mode Exit fullscreen mode

4. You want loose coupling

Especially across application boundaries such as:

Controller
   ↓
Application Service
   ↓
Interface
   ↓
Implementation
Enter fullscreen mode Exit fullscreen mode

5. You expect multiple implementations

For example:

IPaymentProcessor
    ├── StripePaymentProcessor
    ├── RazorpayPaymentProcessor
    └── MockPaymentProcessor
Enter fullscreen mode Exit fullscreen mode

14. When Should You Use an Abstract Class?

Prefer an abstract class when:

1. There is a strong "is-a" relationship

PdfProcessor is a FileProcessor
ExcelProcessor is a FileProcessor
Enter fullscreen mode Exit fullscreen mode

2. Subclasses need shared implementation

protected void Validate()
{
}
Enter fullscreen mode Exit fullscreen mode

3. Subclasses need shared state

protected readonly ILogger Logger;
Enter fullscreen mode Exit fullscreen mode

4. You want to enforce a common workflow

For example:

Validate
   ↓
Authenticate
   ↓
Process
   ↓
Audit
Enter fullscreen mode Exit fullscreen mode

This is commonly implemented using the Template Method pattern.

5. You control the inheritance hierarchy

An abstract base class creates a stronger relationship than an
interface, so it should be used deliberately.


15. A Real Enterprise Example

Imagine an order-processing platform.

We might have:

IOrderProcessor
Enter fullscreen mode Exit fullscreen mode

as the contract:

public interface IOrderProcessor
{
    Task ProcessAsync(Order order);
}
Enter fullscreen mode Exit fullscreen mode

Different implementations:

OnlineOrderProcessor
StoreOrderProcessor
PartnerOrderProcessor
Enter fullscreen mode Exit fullscreen mode

Now suppose every processor needs common behavior:

Validate order
Create correlation ID
Start telemetry
Execute processing
Audit result
Enter fullscreen mode Exit fullscreen mode

An abstract class could provide that workflow:

public abstract class OrderProcessorBase
{
    public async Task ProcessAsync(Order order)
    {
        Validate(order);

        await ProcessCoreAsync(order);

        Audit(order);
    }

    protected virtual void Validate(Order order)
    {
        // Common validation
    }

    protected abstract Task ProcessCoreAsync(Order order);

    protected virtual void Audit(Order order)
    {
        // Common audit
    }
}
Enter fullscreen mode Exit fullscreen mode

A concrete processor then implements only the variable portion:

public class OnlineOrderProcessor : OrderProcessorBase
{
    protected override Task ProcessCoreAsync(Order order)
    {
        // Online-order-specific logic
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

This gives us:

Common workflow
      ↓
Abstract base class
      ↓
Specialized implementations
Enter fullscreen mode Exit fullscreen mode

If consumers should depend on a stable contract, we can additionally
expose an interface:

IOrderProcessor
       ↑
OrderProcessorBase
       ↑
OnlineOrderProcessor
Enter fullscreen mode Exit fullscreen mode

The two mechanisms are not competitors.

They can work together.


16. Interface + Abstract Class Together

This is a powerful enterprise pattern.

public interface IFileProcessor
{
    Task ProcessAsync(string filePath);
}
Enter fullscreen mode Exit fullscreen mode

Then:

public abstract class FileProcessorBase : IFileProcessor
{
    public async Task ProcessAsync(string filePath)
    {
        Validate(filePath);

        await ProcessCoreAsync(filePath);

        Audit(filePath);
    }

    protected abstract Task ProcessCoreAsync(string filePath);

    protected virtual void Validate(string filePath)
    {
        // Common validation
    }

    protected virtual void Audit(string filePath)
    {
        // Common auditing
    }
}
Enter fullscreen mode Exit fullscreen mode

Concrete implementation:

public class PdfProcessor : FileProcessorBase
{
    protected override Task ProcessCoreAsync(string filePath)
    {
        // PDF processing
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we have:

Interface

Defines the external contract
Enter fullscreen mode Exit fullscreen mode

Abstract class

Provides reusable implementation
Enter fullscreen mode Exit fullscreen mode

Concrete class

Provides specific behavior
Enter fullscreen mode Exit fullscreen mode

This is often cleaner than trying to force one abstraction to do
everything.


17. The "Can Do" vs "Is A" Rule

A classic rule is:

Interface = can do

Abstract class = is a

For example:

Bird
Enter fullscreen mode Exit fullscreen mode

A bird:

is a Bird
Enter fullscreen mode Exit fullscreen mode

But:

IFlyable
Enter fullscreen mode Exit fullscreen mode

means:

can fly
Enter fullscreen mode Exit fullscreen mode

So:

public abstract class Bird
{
    public abstract void Eat();
}
Enter fullscreen mode Exit fullscreen mode

and:

public interface IFlyable
{
    void Fly();
}
Enter fullscreen mode Exit fullscreen mode

Then:

public class Eagle : Bird, IFlyable
{
    public override void Eat()
    {
    }

    public void Fly()
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

But be careful.

The "is-a / can-do" rule is a useful mental model, not an absolute law.

Modern software design often focuses more on:

  • Responsibilities
  • Coupling
  • Composition
  • Polymorphism
  • Reuse
  • Changeability

than on inheritance terminology alone.


18. Composition vs Inheritance

One of the most important design l

Top comments (0)