Interfaces, Abstract Classes, and DIMs in C#: A Real-World Developer’s Perspective

As a C# developer, one of the most debated design decisions I’ve made (and seen others struggle with) is this:

“Should I use an interface, an abstract class, or a mix of both?”

It’s not just about syntax or style, it’s about shaping the future of your application. If you’re building an extensible API, working on a SaaS platform, or simply trying to write better C# code, this question can dictate how testable, maintainable, and resilient your solution becomes.

In this post, I’ll walk you through my personal learnings (and scars) from dealing with these choices, with links to in-depth posts for those who want to go deeper.

Interfaces: The Contract That Keeps Things Decoupled

Think of interfaces as promises, they define what a class can do without caring how it does it.

For example, an INotificationService contract can be implemented via Email, SMS, Push, or even Slack, and your OrderService doesn’t need to know or care about the concrete type.

public interface INotificationService
{
    Task SendAsync(string recipient, string subject, string message);
}

This is the backbone of loose coupling and dependency injection, which makes your code testable and adaptable. Add a mock during testing, or swap implementations in production, no need to rewrite business logic.

Dive deeper into interfaces: Interface Contracts in C#: Decouple Code, Improve Testing, Embrace DI

Abstract Classes: Share Logic When There’s a Real Is-A Relationship

Interfaces are great for “can-do” behaviors, but sometimes you want to enforce structure and also provide shared logic.

That’s where abstract classes shine.

Imagine you’re building a Document processing system where every type of document (PDF, Word, Excel) needs to be saved, validated, and tagged, but with different logic. Rather than duplicate common logic, you define an abstract base:

public abstract class Document
{
    public string Title { get; set; }

    public void UpdateMetadata(string title) => Title = title;

    public abstract void Save(string filePath);
}

Derived types implement the abstract methods, but reuse the shared methods.

You’ll want to read: C# Abstract Classes: Real Examples, Patterns, and Best Practices

Interfaces with Default Implementations (DIMs): Friend or Foe?

Since C# 8.0, interfaces can have default implementations. Sounds great, right? You get some logic without breaking existing classes.

But default interface methods are not a replacement for abstract classes.

Here’s why:

  • You can’t have state or fields in interfaces.
  • All methods are public.
  • No constructors = no dependency injection.

Use DIMs for API evolution, like adding optional behavior without breaking older code.

public interface ILogger
{
    void Log(string message);

    void LogWithTimestamp(string message) =>
        Log($"[{DateTime.UtcNow}] {message}");
}

You get flexibility without forcing all implementers to change.

Explore this more: C# Default Interface Methods: Future-Proof and Backward-Compatible APIs

When to Use What?

Here’s how I decide:

Use Case Prefer Interface Prefer Abstract Class
Multiple implementations needed (e.g., Email, SMS) YES NO
Shared logic or base workflow (e.g., database provider) NO YES
Requires constructor injection or state NO YES
Want to evolve API without breaking changes YES (with DIMs) YES (with virtual methods)
Testability / mocking YES HARD (test via derived concrete class)

Need more clarity? I created a breakdown of 10 real-world questions you should ask:
C# Abstract Class vs Interface: 10 Real-World Questions You Should Ask

Common Mistakes

  • Overusing interfaces: Don’t abstract for the sake of it. If there’s only one implementation, an interface may add unnecessary indirection.
  • Abusing DIMs: Keep default methods simple, don’t bake in business logic that can’t be tested or customized.
  • Using abstract classes in plugin systems: You limit flexibility. Prefer interfaces for plugin-style extensibility.

Final Thoughts

Use interfaces when you want flexibility and testability. Use abstract classes when you want shared behavior and structured inheritance. And use default interface methods when evolving APIs without breaking clients.

Each tool has its place. Mastering when to use what is the difference between duct-taped code and clean, scalable systems.

Want to learn more? Here are the full deep-dive posts this summary links to:

Let me know in the comments: What do you reach for first, interfaces or abstract classes?

Similar Posts