C#  

Interfaces in C#: The Complete Practical Guide Every .NET Developer Should Master

Introduction

Think about the last time you charged your phone.

You didn't care what was happening inside the charger. You didn't ask the socket how it generates electricity. You just plugged in the charger, and it worked.

Now think about a car steering wheel. Every car — petrol, diesel, electric, automatic, manual — uses a steering wheel the same way. Turn it left, the car goes left. You don't need to know what's happening underneath: hydraulics, electric motors, or mechanical linkages. The steering wheel gives you a promise: "Turn me, and the car will turn." How it fulfills that promise is not your problem.

This is exactly what an Interface does in C#.

An interface defines WHAT should happen. It never tells you HOW it happens. The "how" is left to whoever implements it.

This one idea — separating "what" from "how" — is one of the most powerful ideas in all of software engineering. And once you truly understand it, a huge number of things in .NET suddenly make sense: Dependency Injection, Repository Pattern, Unit Testing, SOLID Principles, Clean Architecture — all of them are built on top of interfaces.

If you're a C# developer and you've been writing interfaces just because "that's how it's done," this article is for you. We're going deep — not textbook deep, but real-project deep. By the end, you won't just know what an interface is. You'll know why every senior .NET developer reaches for one, and when NOT to.

Let's get started.

What is an Interface?

In the simplest words:

An interface is a contract.

A contract says, "If you sign me, you must do these things." It doesn't care how you do them — it only cares that you do them.

In C#, an interface defines a set of methods, properties, events, or indexers — without any implementation. Any class or struct that implements the interface must provide the actual code for those members.

Here's the basic shape of an interface:

public interface IVehicle
{
    void Start();
    void Stop();
}

This says: "Any class that becomes an IVehicle must know how to Start() and Stop()." It doesn't say how — a Car might start with a key, a Bike might start with a kick, a Tesla might start with an app. The interface doesn't care. That's the implementer's job.

Why do interfaces exist?

Because in real software, you rarely write code for just one scenario. You write code that needs to work with different implementations over time — different databases, different payment gateways, different notification services. If your code depends directly on one specific class, you're stuck the moment that class needs to change.

Interfaces let you depend on a promise, not a person. And promises are much easier to swap out than people.

💡 Tip: Whenever you're unsure if you need an interface, ask yourself: "Will this ever have more than one way of being done?" If yes, an interface is probably a good idea.

Why Do We Need Interfaces?

Let's make this real with a small Employee Management example — something close to what most of us build every day.

The Problem: Tightly Coupled Code

Imagine you're building a system that sends a notification whenever a new employee joins. Your first instinct might look like this:

public class EmailService
{
    public void SendEmail(string to, string message)
    {
        Console.WriteLine($"Email sent to {to}: {message}");
    }
}

public class EmployeeService
{
    private readonly EmailService _emailService = new EmailService();

    public void OnboardEmployee(string employeeEmail)
    {
        // business logic for onboarding
        _emailService.SendEmail(employeeEmail, "Welcome to the company!");
    }
}

This looks fine — until three months later, when your manager says: "We now also need to send SMS notifications for employees who don't have an email."

Suddenly, you're stuck. EmployeeService is hard-wired to EmailService. To add SMS, you'd need to touch EmployeeService itself, add if-else conditions, and slowly turn a clean class into a tangled mess.

This is called tight coupling — one class knows too much about another class's exact implementation.

Other problems with this approach:

  • You cannot test EmployeeService without actually sending a real email.

  • You cannot swap EmailService for a MockEmailService during unit testing.

  • Every new notification channel means editing existing, already-tested code (this breaks the Open/Closed Principle, one of the SOLID principles we'll cover later).

The Solution: Introduce an Interface

public interface INotificationService
{
    void Send(string to, string message);
}

public class EmailService : INotificationService
{
    public void Send(string to, string message)
    {
        Console.WriteLine($"Email sent to {to}: {message}");
    }
}

public class SmsService : INotificationService
{
    public void Send(string to, string message)
    {
        Console.WriteLine($"SMS sent to {to}: {message}");
    }
}

public class EmployeeService
{
    private readonly INotificationService _notificationService;

    public EmployeeService(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }

    public void OnboardEmployee(string contact)
    {
        _notificationService.Send(contact, "Welcome to the company!");
    }
}

Now EmployeeService doesn't care whether it's talking to email, SMS, WhatsApp, or a service that doesn't exist yet. It only knows: "Give me something that can Send()."

Want to add WhatsApp notifications next year? Just create a WhatsAppService : INotificationService. EmployeeService doesn't change at all.

This is the power of interfaces: they let your code depend on behavior, not on specific implementations.

Best Practice: When a class depends on another class only to use its behavior (not its internal data), consider depending on an interface instead of the concrete class.

Syntax of Interface

Let's slow down and look at the actual syntax pieces, one at a time.

public interface IEmployee
{
    // Property
    string Name { get; set; }

    // Method
    decimal CalculateSalary();

    // Event
    event EventHandler OnPromotion;

    // Indexer
    string this[int index] { get; }

    // Default interface method (C# 8+)
    void PrintDetails()
    {
        Console.WriteLine($"Employee: {Name}");
    }

    // Static member (C# 11+)
    static string CompanyName = "Esskae Solutions";
}

Let's break each keyword down:

Keyword / MemberMeaning
interfaceDeclares the type as an interface, not a class
public interfaceMakes the interface accessible from any assembly (interfaces are usually public)
MethodsDefine behavior — the "verbs" of the contract (CalculateSalary())
PropertiesDefine data the implementer must expose (Name)
EventsDefine notifications the implementer must be able to raise (OnPromotion)
IndexersAllow implementing classes to be accessed like arrays (employee[0])
Default Interface MethodsProvide a default body so implementers don't have to override it (introduced in C# 8)
Static membersShared data or logic tied to the interface itself, not any instance (introduced in later C# versions)

Notice something important: an interface never has a constructor and never stores instance state (fields). It only describes what must exist, not how it's stored.

Common Mistake: Beginners often try to add a private field directly to an interface. You can't — interfaces don't hold instance state. If you need state, that belongs in the implementing class.

First Interface Example: ICalculator

Let's build something simple and complete, so you can see the entire lifecycle — interface, implementation, and usage.

// Step 1: Define the contract
public interface ICalculator
{
    double Add(double a, double b);
    double Subtract(double a, double b);
    double Multiply(double a, double b);
    double Divide(double a, double b);
}
// Step 2: Implement the contract
public class Calculator : ICalculator
{
    public double Add(double a, double b) => a + b;

    public double Subtract(double a, double b) => a - b;

    public double Multiply(double a, double b) => a * b;

    public double Divide(double a, double b)
    {
        if (b == 0)
            throw new DivideByZeroException("Cannot divide by zero.");

        return a / b;
    }
}
// Step 3: Use it
public class Program
{
    public static void Main()
    {
        ICalculator calculator = new Calculator();

        Console.WriteLine($"Add: {calculator.Add(10, 5)}");
        Console.WriteLine($"Subtract: {calculator.Subtract(10, 5)}");
        Console.WriteLine($"Multiply: {calculator.Multiply(10, 5)}");
        Console.WriteLine($"Divide: {calculator.Divide(10, 5)}");
    }
}

Output:

Add: 15
Subtract: 5
Multiply: 50
Divide: 2

Let's understand line by line:

  • ICalculator calculator = new Calculator(); — We're declaring the variable as the interface type, not the class type. This is intentional. It means our code only knows about the contract, not the concrete class. Tomorrow, if we build a ScientificCalculator : ICalculator, this line barely changes.

  • Each method in Calculator must exist because ICalculator demands it. If you forget one, the code won't even compile — the compiler enforces the contract for you.

💡 Real Project Insight: In real projects, you almost never write new Calculator() directly inside your business logic. Instead, this object gets created by the Dependency Injection container and handed to you automatically. We'll cover exactly how that works shortly.

Interface vs Abstract Class

This is one of the most asked interview questions — and for good reason. Both let you define a contract, but they solve different problems.

AspectInterfaceAbstract Class
PurposeDefines what must be done (pure contract)Defines what and optionally some of how
ImplementationNo implementation (except default interface methods)Can have both abstract and fully implemented methods
ConstructorsNot allowedAllowed
FieldsNot allowed (only static, C# 11+)Allowed
PropertiesOnly declarations (or default bodies)Full implementation allowed
Multiple InheritanceA class can implement many interfacesA class can inherit only one abstract class
Access ModifiersMembers are implicitly publicMembers can be private, protected, public
VersioningAdding a new member breaks all implementers (unless default method)Adding a new method with default logic doesn't break subclasses
When to UseTo define a capability/behavior shared across unrelated classesTo share common code and state among closely related classes
Real ExampleIDisposable, IComparable — unrelated classes share behaviorStream (base for FileStream, MemoryStream) — genuinely related types

A Simple Way to Remember It

  • Interface answers: "What can you do?" (IFlyable, ISwimmable)

  • Abstract class answers: "What are you, fundamentally?" (Animal, Employee)

Example:

public interface IFlyable
{
    void Fly();
}

public interface ISwimmable
{
    void Swim();
}

public abstract class Bird
{
    public string Name { get; set; }

    public abstract void MakeSound();

    public void Eat()
    {
        Console.WriteLine($"{Name} is eating.");
    }
}

public class Duck : Bird, IFlyable, ISwimmable
{
    public override void MakeSound() => Console.WriteLine("Quack!");
    public void Fly() => Console.WriteLine($"{Name} is flying.");
    public void Swim() => Console.WriteLine($"{Name} is swimming.");
}

A Duck is a Bird (abstract class relationship) — but it can Fly and can Swim (interface capabilities). This is exactly how real-world hierarchies work, and C# lets you model both at once.

🎯 Interview Tip: If you're asked "Why does C# not support multiple class inheritance but supports multiple interface implementation?" — the answer is the Diamond Problem. If two abstract classes both had a method with a body, and a class inherited from both, the compiler wouldn't know which implementation to use. Interfaces (without a body, historically) avoided this ambiguity entirely.

Interface Inheritance

Interfaces can inherit from other interfaces — this lets you build up more specific contracts from general ones.

public interface IEmployee
{
    string Name { get; set; }
    decimal BasicSalary { get; set; }
    void ShowDetails();
}

public interface IManager : IEmployee
{
    int TeamSize { get; set; }
    void ConductMeeting();
}

public interface IHRManager : IManager
{
    void ApproveLeave(string employeeName);
}

Now, any class implementing IHRManager must fulfill all three levels of the contract — IEmployee, IManager, and IHRManager.

public class HRManager : IHRManager
{
    public string Name { get; set; }
    public decimal BasicSalary { get; set; }
    public int TeamSize { get; set; }

    public void ShowDetails() =>
        Console.WriteLine($"{Name}, Salary: {BasicSalary}");

    public void ConductMeeting() =>
        Console.WriteLine($"{Name} is conducting a team meeting.");

    public void ApproveLeave(string employeeName) =>
        Console.WriteLine($"{Name} approved leave for {employeeName}.");
}

This models a real HRMS hierarchy beautifully: every HR Manager is a Manager, and every Manager is an Employee — but each level adds its own specific responsibilities.

Multiple Interface Implementation

C# does not allow a class to inherit from multiple classes — but it happily allows a class to implement multiple interfaces. Why the difference?

Because interfaces don't carry implementation or state (in the traditional sense), there's no ambiguity about "which parent's code runs." Each interface just adds a new promise, and the class is responsible for keeping all of them.

public interface IPrintable
{
    void Print();
}

public interface IEmailable
{
    void SendEmail();
}

public interface IArchivable
{
    void Archive();
}

public class Invoice : IPrintable, IEmailable, IArchivable
{
    public void Print() => Console.WriteLine("Invoice printed.");
    public void SendEmail() => Console.WriteLine("Invoice emailed to client.");
    public void Archive() => Console.WriteLine("Invoice archived.");
}

An Invoice can be printed, emailed, and archived — three completely unrelated capabilities, cleanly separated into three interfaces instead of one giant class doing everything.

Best Practice: If a class needs to implement 6-7 interfaces just to satisfy one workflow, take a step back. It might be a sign your interfaces are too fine-grained, or your class is doing too much. Balance is key.

Dependency Injection: Where Interfaces Truly Shine

If you've worked with ASP.NET Core even briefly, you've already used interfaces dozens of times — often without realizing it.

public class LeaveController : ControllerBase
{
    private readonly ILogger<LeaveController> _logger;
    private readonly IConfiguration _configuration;

    public LeaveController(ILogger<LeaveController> logger, IConfiguration configuration)
    {
        _logger = logger;
        _configuration = configuration;
    }
}

ILogger, IConfiguration — these are interfaces. You never wrote new Logger() or new Configuration() yourself. ASP.NET Core's Dependency Injection (DI) container created the actual implementation behind the scenes and handed it to you.

Why does ASP.NET Core rely on interfaces so heavily?

Because the framework needs to be flexible. It doesn't know (and doesn't want to know) exactly how you'll log things, where your configuration comes from, or which database you'll use. So it defines interfaces — ILogger, IConfiguration, IServiceCollection, IHost, IOptions<T> — and lets you (or Microsoft) plug in the actual implementation.

How Dependency Injection Actually Works

// Step 1: Define the interface
public interface IEmployeeRepository
{
    Employee GetById(int id);
    void Add(Employee employee);
}

// Step 2: Implement it
public class EmployeeRepository : IEmployeeRepository
{
    public Employee GetById(int id) => new Employee { Id = id, Name = "Sudarshan" };
    public void Add(Employee employee) => Console.WriteLine("Employee added.");
}

// Step 3: Register it in Program.cs
builder.Services.AddScoped<IEmployeeRepository, EmployeeRepository>();

// Step 4: Ask for it — DI hands it to you automatically
public class EmployeeService
{
    private readonly IEmployeeRepository _repository;

    public EmployeeService(IEmployeeRepository repository)
    {
        _repository = repository;
    }
}

You never manually created EmployeeRepository. You just told the container: "Whenever someone asks for IEmployeeRepository, give them an EmployeeRepository." That's the whole trick.

Service Lifetimes

LifetimeMeaningWhen to Use
TransientA new instance is created every time it's requestedLightweight, stateless services (e.g., a simple calculator service)
ScopedOne instance per HTTP requestMost repository and DB-related services (e.g., IEmployeeRepository)
SingletonOne instance for the entire application's lifetimeShared, expensive-to-create, thread-safe services (e.g., caching services, configuration)
builder.Services.AddTransient<ICalculator, Calculator>();
builder.Services.AddScoped<IEmployeeRepository, EmployeeRepository>();
builder.Services.AddSingleton<ICacheService, CacheService>();

Warning: Injecting a Scoped service into a Singleton service is a classic mistake that leads to subtle, hard-to-debug bugs (the Scoped instance gets "captured" for the entire app lifetime instead of per-request). Always be mindful of lifetime mismatches.

Why does all of this depend on interfaces? Because DI is fundamentally about swapping implementations without touching consuming code — and that's only possible if the consuming code depends on a contract, not a concrete class.

Repository Pattern: Interfaces in Real Project Architecture

Let's build a complete, realistic example using an Employee Management System.

public interface IEmployeeRepository
{
    Employee GetById(int id);
    IEnumerable<Employee> GetAll();
    void Add(Employee employee);
    void Update(Employee employee);
    void Delete(int id);
}

Real implementation — talks to the actual database:

public class EmployeeRepository : IEmployeeRepository
{
    private readonly AppDbContext _context;

    public EmployeeRepository(AppDbContext context)
    {
        _context = context;
    }

    public Employee GetById(int id) => _context.Employees.Find(id);
    public IEnumerable<Employee> GetAll() => _context.Employees.ToList();
    public void Add(Employee employee) => _context.Employees.Add(employee);
    public void Update(Employee employee) => _context.Employees.Update(employee);
    public void Delete(int id)
    {
        var employee = _context.Employees.Find(id);
        if (employee != null) _context.Employees.Remove(employee);
    }
}

Fake implementation — used for testing, no database needed:

public class FakeEmployeeRepository : IEmployeeRepository
{
    private readonly List<Employee> _employees = new()
    {
        new Employee { Id = 1, Name = "Sudarshan" },
        new Employee { Id = 2, Name = "Pari" }
    };

    public Employee GetById(int id) => _employees.FirstOrDefault(e => e.Id == id);
    public IEnumerable<Employee> GetAll() => _employees;
    public void Add(Employee employee) => _employees.Add(employee);
    public void Update(Employee employee) { /* update in list */ }
    public void Delete(int id) => _employees.RemoveAll(e => e.Id == id);
}

Notice: EmployeeService (the consumer of IEmployeeRepository) doesn't need to change at all whether it's talking to the real database or the fake in-memory list. This is exactly what makes unit testing painless.

Unit Testing: Why Interfaces Make Testing Painless

Imagine testing EmployeeService without an interface — you'd need a real database connection just to run a simple test. That's slow, fragile, and honestly, painful.

With interfaces, we can use a mocking framework like Moq to create a fake IEmployeeRepository on the fly, with exactly the behavior we want for that specific test.

using Moq;
using Xunit;

public class EmployeeServiceTests
{
    [Fact]
    public void GetEmployeeById_ReturnsCorrectEmployee()
    {
        // Arrange
        var mockRepo = new Mock<IEmployeeRepository>();
        mockRepo.Setup(repo => repo.GetById(1))
                .Returns(new Employee { Id = 1, Name = "Sudarshan" });

        var service = new EmployeeService(mockRepo.Object);

        // Act
        var result = service.GetEmployee(1);

        // Assert
        Assert.Equal("Sudarshan", result.Name);
    }
}

We never touched a real database. mockRepo.Object behaves exactly like IEmployeeRepository, but we control precisely what it returns. This is called dependency isolation — testing one unit of code without depending on anything external (databases, APIs, file systems).

💡 Real Project Insight: In our HRMS project, every service class (Leave, Attendance, Payroll) depends on repository interfaces, not concrete classes. This single decision is what let our QA and dev teams write hundreds of fast, reliable unit tests without ever touching the staging database.

SOLID Principles and Interfaces

Interfaces are the backbone of two SOLID principles in particular — but they support all five in some way.

PrincipleHow Interfaces Help
Single ResponsibilityEncourages small, focused interfaces — each with one clear job
Open/ClosedNew behavior = new class implementing the interface, existing code stays untouched
Liskov SubstitutionAny implementation of an interface must be usable wherever the interface is expected
Interface SegregationPush clients to depend only on the methods they actually use (small interfaces, not fat ones)
Dependency InversionHigh-level code depends on interfaces (abstractions), not low-level concrete classes

The last two — Interface Segregation and Dependency Inversion — deserve extra attention.

Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

// BAD: EmployeeService directly depends on a concrete SqlEmployeeRepository
public class EmployeeService
{
    private readonly SqlEmployeeRepository _repository = new SqlEmployeeRepository();
}

// GOOD: EmployeeService depends on the abstraction
public class EmployeeService
{
    private readonly IEmployeeRepository _repository;

    public EmployeeService(IEmployeeRepository repository)
    {
        _repository = repository;
    }
}

The "high-level" business logic (EmployeeService) no longer cares about the "low-level" detail (SQL Server, or MongoDB, or an in-memory list). Both sides depend on IEmployeeRepository — the abstraction sits in the middle.

Interface Segregation Principle (ISP)

A class should not be forced to implement methods it doesn't need.

Bad Example

public interface IEmployee
{
    void Work();
    void ApprovePayroll();
    void ConductInterview();
    void ManageTeam();
}

public class JuniorDeveloper : IEmployee
{
    public void Work() => Console.WriteLine("Coding...");

    // Forced to implement methods that make no sense for this role!
    public void ApprovePayroll() => throw new NotImplementedException();
    public void ConductInterview() => throw new NotImplementedException();
    public void ManageTeam() => throw new NotImplementedException();
}

This is a fat interface — it forces every implementer to carry baggage it doesn't need. A junior developer has no business approving payroll, yet the contract demands it.

Good Example

public interface IWorkable
{
    void Work();
}

public interface IPayrollApprover
{
    void ApprovePayroll();
}

public interface IInterviewer
{
    void ConductInterview();
}

public interface ITeamManager
{
    void ManageTeam();
}

public class JuniorDeveloper : IWorkable
{
    public void Work() => Console.WriteLine("Coding...");
}

public class HRManager : IWorkable, IPayrollApprover, IInterviewer, ITeamManager
{
    public void Work() => Console.WriteLine("Reviewing HR policies...");
    public void ApprovePayroll() => Console.WriteLine("Payroll approved.");
    public void ConductInterview() => Console.WriteLine("Interview conducted.");
    public void ManageTeam() => Console.WriteLine("Team managed.");
}

Now each role only implements what it actually needs. Nobody is forced to throw NotImplementedException() — which, honestly, is one of the biggest smells in any codebase.

Common Mistake: If you find yourself writing throw new NotImplementedException() inside an interface implementation, that's ISP screaming at you. Split the interface.

Real Project Example: A Clean ASP.NET Core Architecture

Here's how interfaces fit into a real, production-grade ASP.NET Core project structure:

HRMS.Solution/
│
├── HRMS.API/
│   └── Controllers/
│       ├── EmployeeController.cs
│       └── LeaveController.cs
│
├── HRMS.Application/
│   ├── Interfaces/
│   │   ├── IEmployeeService.cs
│   │   ├── ILeaveService.cs
│   │   └── INotificationService.cs
│   ├── Services/
│   │   ├── EmployeeService.cs
│   │   ├── LeaveService.cs
│   │   └── NotificationService.cs
│   └── DTOs/
│       ├── EmployeeDto.cs
│       └── LeaveRequestDto.cs
│
├── HRMS.Domain/
│   └── Entities/
│       ├── Employee.cs
│       └── LeaveApplication.cs
│
├── HRMS.Infrastructure/
│   ├── Repositories/
│   │   ├── IEmployeeRepository.cs
│   │   ├── EmployeeRepository.cs
│   │   ├── ILeaveRepository.cs
│   │   └── LeaveRepository.cs
│   └── Data/
│       └── AppDbContext.cs
│
└── HRMS.Tests/
    ├── EmployeeServiceTests.cs
    └── LeaveServiceTests.cs

Why do interfaces live in Services and Repositories, specifically?

  • Repositories talk directly to the database. Interfaces here let us swap SQL Server for another data source (or a fake, for testing) without touching business logic.

  • Services contain business rules. Interfaces here let the API layer (Controllers) depend on behavior, not implementation — and let us mock services during controller-level testing.

Here's a simple flow diagram of how a request travels through this architecture:

graph LR
    A[Controller] -->|depends on| B[IEmployeeService]
    B -->|implemented by| C[EmployeeService]
    C -->|depends on| D[IEmployeeRepository]
    D -->|implemented by| E[EmployeeRepository]
    E -->|talks to| F[(Database)]

Every arrow that crosses a layer boundary goes through an interface, not a concrete class. That's what keeps each layer independently testable and replaceable.

Built-in Interfaces in .NET

.NET itself is built on interfaces — here are the ones you'll run into constantly.

InterfacePurpose
IDisposableMarks a class that holds unmanaged resources needing cleanup (Dispose()) — used with using statements
IEnumerable / IEnumerable<T>Allows a collection to be iterated with foreach
IEnumerator / IEnumerator<T>Provides the actual iteration logic (MoveNext(), Current)
IComparable / IComparable<T>Allows objects to be compared for sorting (CompareTo())
IComparer / IComparer<T>Defines a custom comparison strategy, separate from the object itself
ICloneableAllows an object to create a copy of itself
IAsyncDisposableAsync version of IDisposable, used with await using
IEquatable<T>Defines strongly-typed equality logic (Equals()) without boxing
IFormFileRepresents an uploaded file in ASP.NET Core
ILogger / ILogger<T>Standard logging abstraction across .NET
IConfigurationAccess to app settings, environment variables, and configuration sources
IHostedServiceDefines a background service that starts/stops with the app
IServiceProviderResolves registered services from the DI container
IServiceScopeRepresents a scoped lifetime boundary for DI
IServiceCollectionUsed to register services into the DI container
IQueryable / IQueryable<T>Represents a query that can be translated to SQL (used by EF Core)
IObservable<T>Represents a stream of data that can be observed (used with Reactive Extensions)
IObserver<T>Represents the consumer that reacts to data pushed from an IObservable<T>
public class EmployeeReader : IDisposable
{
    private FileStream _stream = new FileStream("employees.txt", FileMode.Open);

    public void Dispose()
    {
        _stream?.Dispose();
        Console.WriteLine("Resources released.");
    }
}

// Usage
using (var reader = new EmployeeReader())
{
    // work with the file
} // Dispose() is called automatically here

💡 Tip: Any time your class holds onto a database connection, file handle, or network stream, implement IDisposable. It's one of the most overlooked but important interfaces in production code.

Default Interface Methods (C# 8+)

Before C# 8, adding a new method to an interface would break every single class that implemented it — because now they were all missing a required method.

Microsoft solved this with default interface methods — a method inside an interface that has a body, so implementers don't have to override it unless they want custom behavior.

public interface INotificationService
{
    void Send(string to, string message);

    // Default implementation
    void SendUrgent(string to, string message)
    {
        Send(to, $"[URGENT] {message}");
    }
}

public class EmailService : INotificationService
{
    public void Send(string to, string message) =>
        Console.WriteLine($"Email to {to}: {message}");

    // No need to implement SendUrgent — it uses the default!
}

Why Microsoft introduced this: mainly to let library authors (like the .NET team itself) add new members to existing interfaces without breaking every consumer's code in a new version.

Advantages:

  • Allows interfaces to evolve without breaking existing implementations

  • Reduces duplicate code for common default behavior

Disadvantages:

  • Can blur the line between interfaces and abstract classes

  • Overusing this can confuse readers about where the "real" logic lives

Warning: Default interface methods are a great escape hatch for library evolution — but in your own application code, don't lean on them heavily. It's often clearer to keep interfaces as pure contracts and put shared logic in a base class or helper instead.

Static Interface Members (C# 11+)

Newer C# versions allow interfaces to declare static members, including static abstract methods — this is especially useful for generic math and validation-style scenarios.

public interface IValidator<T>
{
    static abstract bool Validate(T value);
}

public class AgeValidator : IValidator<int>
{
    public static bool Validate(int value) => value >= 18 && value <= 60;
}

This feature mainly powers advanced generic scenarios (like .NET's generic math support) — you likely won't reach for it every day, but it's good to know it exists, especially in interviews.

Generic Interfaces

Generic interfaces let you write a contract once and reuse it across many types — this is where interfaces and generics work beautifully together.

public interface IRepository<T> where T : class
{
    T GetById(int id);
    IEnumerable<T> GetAll();
    void Add(T entity);
    void Update(T entity);
    void Delete(int id);
}

public class EmployeeRepository : IRepository<Employee>
{
    public Employee GetById(int id) => new Employee { Id = id };
    public IEnumerable<Employee> GetAll() => new List<Employee>();
    public void Add(Employee entity) { }
    public void Update(Employee entity) { }
    public void Delete(int id) { }
}

public class LeaveRepository : IRepository<LeaveApplication>
{
    public LeaveApplication GetById(int id) => new LeaveApplication { Id = id };
    public IEnumerable<LeaveApplication> GetAll() => new List<LeaveApplication>();
    public void Add(LeaveApplication entity) { }
    public void Update(LeaveApplication entity) { }
    public void Delete(int id) { }
}

One IRepository<T> contract, reused for Employee, LeaveApplication, or any other entity — without duplicating the same five methods over and over.

Other generic interfaces you use constantly, often without noticing:

InterfacePurpose
IEnumerable<T>Type-safe iteration
IList<T>An ordered, indexable collection
ICollection<T>A collection that supports add/remove/count
IDictionary<TKey, TValue>Key-value pair storage

Best Practice: When building repository or service layers for multiple entities, always reach for a generic interface first (IRepository<T>) instead of writing near-identical interfaces for every entity.

Common Mistakes Developers Make with Interfaces

  • Too many interfaces, too soon. Not every class needs an interface. If a class has exactly one implementation and you have no plan (or realistic need) to swap it, an interface might just be extra ceremony.

  • One implementation forever. If you created IEmployeeService two years ago and EmployeeService is still its only implementation, ask yourself honestly — is the interface earning its keep, or is it just boilerplate?

  • Marker interfaces (empty interfaces used just as a "tag"). These were common before C# had attributes, but today, attributes usually express intent more clearly than an empty interface.

  • Fat interfaces. Interfaces with 10+ unrelated methods usually violate Interface Segregation. Split them.

  • Breaking ISP for "convenience." Cramming unrelated behavior into one interface because "it's easier" always costs more later.

  • Using interfaces where a simple class would do. Not every dependency needs to be swappable. A DateTime wrapper or a simple string formatter rarely needs an interface.

  • Poor naming. In C#, interfaces conventionally start with I (IEmployeeService, not EmployeeServiceInterface or EmployeeServiceContract). Stick to convention — it helps every developer instantly recognize a contract.

  • Versioning without care. Adding a new method to a widely-implemented interface (without a default body) breaks every implementer. Plan interface changes carefully, especially in shared libraries.

Common Mistake: "I'll add an interface just in case we need to swap it later" is a trap. YAGNI (You Aren't Gonna Need It) applies here too. Add the interface when the second implementation actually shows up, or when you specifically need it for testing.

Performance Considerations

A common question: "Do interfaces slow down my application?"

Short answer: in almost all real-world applications, no — not in any way that matters.

Here's what's actually happening under the hood:

  • Virtual Dispatch: Calling a method through an interface reference involves a small extra lookup step (the runtime figures out which concrete implementation to call). This is called virtual dispatch, and it costs a few nanoseconds.

  • JIT and Inlining: The .NET JIT compiler is very good at optimizing these calls, and in many cases can even inline them, especially when the concrete type is known at compile time.

  • Memory: Interfaces themselves don't add extra memory overhead to objects. A class implementing an interface doesn't become "bigger" because of it.

Performance myths, busted:

MythReality
"Interfaces are always slower than concrete classes"The difference is measured in nanoseconds for the vast majority of applications — completely irrelevant compared to database calls, network I/O, or JSON serialization
"Avoid interfaces in hot paths for performance"Only relevant in extreme, latency-critical scenarios (like high-frequency trading systems or game engines) — not typical business/web applications
"DI makes apps slow because of interfaces"DI overhead comes mostly from object creation/resolution, not the interface abstraction itself, and it's negligible for 99% of applications

Should we always use interfaces? No. The decision should be based on design needs — testability, swappability, decoupling — not performance. If you're optimizing at the nanosecond level for interface dispatch before you've optimized your database queries, you're solving the wrong problem.

🎯 Performance Tip: Profile before you optimize. In 9 out of 10 real .NET applications, your actual bottleneck will be a database query or an external API call — never the cost of calling a method through an interface.

Best Practices for Using Interfaces

  1. Name interfaces with the I prefixIEmployeeService, not EmployeeServiceInterface.

  2. Keep interfaces small and focused — follow Interface Segregation from the start.

  3. Design interfaces around behavior, not data — an interface should describe what an object does, not what it has.

  4. Add an interface when you have a real reason — a second implementation, or a genuine testing need. Don't add it "just in case."

  5. Avoid one-interface-per-class by default — it's a common but unnecessary habit; only add interfaces where they earn their keep.

  6. Use default interface methods sparingly — mainly for library evolution, not everyday app logic.

  7. Depend on abstractions in your business logic layer — services should depend on interfaces, not concrete repositories or external SDKs directly.

  8. Register interfaces with correct DI lifetimes — Transient, Scoped, Singleton — mismatches cause subtle bugs.

  9. Don't leak implementation details into the interface — an IEmployeeRepository shouldn't expose SQL-specific methods.

  10. Use generic interfaces to avoid duplicationIRepository<T> instead of IEmployeeRepository, ILeaveRepository, etc., when the operations are truly identical.

  11. Document the contract clearly — XML comments on interface methods pay off massively for teams.

  12. Avoid throw new NotImplementedException() in production interfaces — it's a signal the interface needs splitting.

  13. Group related interfaces together in your project structure (e.g., a dedicated Interfaces folder or namespace).

  14. Prefer constructor injection over service locator patterns — always ask for dependencies explicitly through the constructor.

  15. Write interfaces from the consumer's perspective — think about what the caller needs, not what's convenient to implement.

  16. Version interfaces carefully in shared libraries — breaking changes ripple out to every consumer.

  17. Use interfaces to enable parallel development — once a team agrees on IPaymentGateway, front-end/API teams and payment-integration teams can work independently.

  18. Avoid interface bloat in Controllers — controllers should depend on a handful of well-defined service interfaces, not dozens of granular ones.

  19. Keep async and sync methods separate — don't mix Task<T> and non-task returning methods carelessly in the same interface; be consistent.

  20. Review interfaces during code review just like classes — a badly designed interface is often harder to fix later than a badly designed class, because more code depends on it.

Summary: Key Takeaways

Let's bring it all together:

  • An interface is a contract — it defines what must be done, never how.

  • Interfaces solve the problem of tight coupling, letting your code depend on behavior instead of specific classes.

  • Dependency Injection, Repository Pattern, and Unit Testing in .NET are all built on the foundation interfaces provide.

  • Interfaces are central to SOLID principles, especially Dependency Inversion and Interface Segregation.

  • Use interfaces when you have a real reason — multiple implementations, testability needs, or genuine architectural boundaries. Don't add them just out of habit.

  • .NET itself — ILogger, IEnumerable, IDisposable, IConfiguration — is a living example of how deeply interfaces are woven into the framework's design.

  • Performance concerns around interfaces are, in almost all real applications, a myth. Design for clarity and testability first.

If you take away just one idea from this entire article, let it be this: an interface is a promise your code makes — and every well-designed .NET application is really just a collection of promises, kept by different implementations, wired together cleanly.

Start small. Pick one class in your current project that's tightly coupled to another. Introduce an interface. Write one test using a mock. You'll feel the difference immediately — and you'll never look at your code the same way again.