Interface Segregation
Vaibhav • September 10, 2025
In the previous article, we explored how multiple interface implementation allows a class to adopt several behaviors at once. Now we turn to a principle that helps us design those interfaces wisely: Interface Segregation. This principle is part of the SOLID design philosophy and focuses on keeping interfaces small, focused, and client-specific. In this article, we’ll explore what interface segregation means in practice, why it matters, how to apply it in C#, and how it helps you avoid bloated, fragile designs.
What is Interface Segregation?
The Interface Segregation Principle (ISP) states that “clients should not be forced to depend on interfaces they do not use.” In simpler terms, an interface should contain only the methods that are relevant to the implementing class. If a class needs only part of an interface, that’s a sign the interface is doing too much.
Let’s look at a problematic example:
interface IMachine
{
void Print();
void Scan();
void Fax();
}
class OldPrinter : IMachine
{
public void Print()
{
Console.WriteLine("Printing...");
}
public void Scan()
{
throw new NotImplementedException();
}
public void Fax()
{
throw new NotImplementedException();
}
}
The OldPrinter
only supports printing, but it’s forced to implement Scan()
and Fax()
because they’re part of the
interface. This violates ISP - the class depends on methods it doesn’t need.
Throwing NotImplementedException
is a red flag. It
usually means the interface is too broad.
How to fix it - split interfaces by capability
The solution is to break large interfaces into smaller, focused ones. Each interface should represent a single capability. Then, classes can implement only the interfaces they need.
interface IPrinter
{
void Print();
}
interface IScanner
{
void Scan();
}
interface IFax
{
void Fax();
}
class OldPrinter : IPrinter
{
public void Print()
{
Console.WriteLine("Printing...");
}
}
Now OldPrinter
only implements IPrinter
, and
isn’t burdened with irrelevant methods. This design is cleaner, more flexible, and easier to maintain.
Interface Segregation in real-world APIs
The .NET framework follows ISP in many places. For example, instead of one giant collection interface, it defines several:
interface IEnumerable
{
IEnumerator GetEnumerator();
}
interface ICollection : IEnumerable
{
int Count { get; }
void CopyTo(Array array, int index);
}
interface IList : ICollection
{
object this[int index] { get; set; }
void Add(object value);
void Remove(object value);
}
This layered design allows you to implement only what you need. If your class just needs to be iterable,
implement IEnumerable
. If it needs indexing, implement IList
. This keeps interfaces lean and focused.
Designing segregated interfaces
When designing your own interfaces, ask yourself:
Does every method belong together? Are there clients that only need part of this interface? Would splitting the interface make it easier to test or mock?
For example, instead of this:
interface IUserService
{
void Register(string username);
void Login(string username, string password);
void ResetPassword(string username);
void SendWelcomeEmail(string username);
}
You might define:
interface IUserRegistration
{
void Register(string username);
}
interface IUserAuthentication
{
void Login(string username, string password);
void ResetPassword(string username);
}
interface IUserNotification
{
void SendWelcomeEmail(string username);
}
Now each interface reflects a distinct responsibility. Classes can implement only what they need, and your code becomes easier to test, mock, and extend.
Interface Segregation and mocking
ISP makes unit testing easier. When interfaces are small and focused, you can create simple mocks that implement just the methods under test. This avoids bloated test doubles and keeps your tests clean.
class MockNotifier : IUserNotification
{
public List<string> SentEmails = new List<string>();
public void SendWelcomeEmail(string username)
{
SentEmails.Add(username);
}
}
This mock only implements IUserNotification
. It doesn’t need to worry about
registration or authentication logic. That’s the power of segregation - each interface is easy to mock and
reason about.
Interface Segregation and dependency injection
ISP also improves dependency injection. When classes depend on small interfaces, you can inject only the services they need. This reduces coupling and makes your constructors simpler.
class WelcomeService
{
private readonly IUserNotification _notifier;
public WelcomeService(IUserNotification notifier)
{
_notifier = notifier;
}
public void SendWelcome(string username)
{
_notifier.SendWelcomeEmail(username);
}
}
The WelcomeService
depends only on IUserNotification
, not the entire IUserService
.
This makes it easier to test, reuse, and evolve independently.
Interface Segregation and versioning
Segregated interfaces are easier to version. If you need to add a new method, you can create a new interface without breaking existing clients.
interface IUserReporting
{
void GenerateReport(string username);
}
Clients that don’t need reporting can ignore this interface. This avoids breaking changes and keeps your API stable.
Common mistakes to avoid
One common mistake is creating “god interfaces” - interfaces with too many unrelated methods. These are hard to implement, hard to mock, and hard to evolve. Another mistake is assuming that inheritance is the only way to share behavior. Interfaces are about contracts, not implementation. Use composition to share logic, and interfaces to define capabilities.
In C# 8.0 and later, interfaces can include default implementations using default interface methods
. This allows you to share logic across
implementations while still keeping interfaces small.
Summary
Interface Segregation is about designing interfaces that are small, focused, and client-specific. You’ve learned how to split large interfaces into modular capabilities, how to apply ISP in real-world APIs, how it improves testing, dependency injection, and versioning, and how to avoid common mistakes. By following this principle, you’ll write code that is easier to understand, easier to test, and easier to evolve.
In the next article, we’ll explore Polymorphism in Practice - how to apply polymorphism effectively in real-world scenarios, and how it interacts with interfaces, inheritance, and design patterns.