The Interface Segregation Principle (ISP) is the “I” in SOLID and one of the five essential SOLID design principles for object-oriented code in C#, VB.NET or Java. These principles are guidelines for the proper usage of object-oriented features. In one sentence, ISP states that:
A client should not be forced to depend on methods it does not use.
It is all about the interface, the common abstraction available in most OOP languages such as C#, VB.NET or Java. A more complete and actionable definition of the ISP is:
ISP splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them. Such shrunken interfaces are also called role interfaces.
This article explains the Interface Segregation Principle in C# with concrete .NET examples: how to spot a fat interface, how to segregate it into role interfaces, how the ISP relates to the rest of SOLID, and where the principle stops being useful. The examples are taken straight from the .NET Base Class Library (BCL), because the framework itself had to learn the ISP the hard way.
SOLID Principles Summary
Before delving into the Interface Segregation Principle, let’s take a quick look at how it stands in the SOLID design principles:
- The Single Responsibility Principle (SRP): A class should have one reason to change. This principle is about how to partition your logic into classes and avoid ending up with some monster classes (known as god class).
- The Open-Close Principle (OCP): Modules should be open for extension and closed for modification. To implement a new feature, it is better to add a new derived class instead of having to modify some existing code.
- The Liskov Substitution Principle (LSP): Methods that use references to base classes must be able to use objects of derived classes without knowing it. Array implements IList<T> but throws NotSupportException on IList<T>.Add(). This is a clear LSP violation.
- The Interface Segregation Principle (ISP): The client should not depend by design on methods it does not use. This is why designers introduced interfaces like IReadOnlyCollection<T>. Often the client just needs a subset of features like read-only access instead of full read-write access.
- The Dependency Inversion Principle (DIP): Depend on abstractions, not on implementations. Interfaces are much less subject to changes than classes, especially the ones that abide by the (ISP) and the (LSP).
The Interface Segregation Principle by Example in C#
The quickest way to feel the ISP is to break it on purpose. Imagine a single interface that describes every operation an office device might support:
|
1 2 3 4 5 |
public interface IOfficeDevice { void Print(Document doc); void Scan(Document doc); void Fax(Document doc); } |
Now a basic printer with no scanner and no fax line is forced to implement two methods it cannot honor:
|
1 2 3 4 5 |
public class BasicPrinter : IOfficeDevice { public void Print(Document doc) { /* real work */ } public void Scan(Document doc) => throw new NotSupportedException(); public void Fax(Document doc) => throw new NotSupportedException(); } |
Those two NotSupportedException are the smell. The interface is too fat for this client, and nothing stops a caller from invoking Scan() on a BasicPrinter and blowing up at runtime. Segregating the fat interface into focused role interfaces fixes it:
|
1 2 3 |
public interface IPrinter { void Print(Document doc); } public interface IScanner { void Scan(Document doc); } public interface IFax { void Fax(Document doc); } |
Each class now implements only the roles it actually fulfills, and a multifunction device simply composes the interfaces it supports:
|
1 2 3 4 5 6 7 8 9 |
public class BasicPrinter : IPrinter { public void Print(Document doc) { /* ... */ } } public class AllInOnePrinter : IPrinter, IScanner, IFax { public void Print(Document doc) { /* ... */ } public void Scan(Document doc) { /* ... */ } public void Fax(Document doc) { /* ... */ } } |
A client that only prints now depends on IPrinter alone. It can no longer call Scan() or Fax() by mistake, and it does not break when the fax behavior changes. That is the whole point of the ISP: shrink the contract down to what the client actually consumes. The rest of this article shows that the very same pressure already shaped the .NET Base Class Library.
C# Example of the Interface Segregation Principle
Since the introduction of generic in .NET and C#, developers are consuming the interface IList<T>. With methods like Add(T), Contains(T), IndexOf(T), and the indexer syntax list[index] this interface represent all sorts of list of items. But often a client only uses a read-only version of a list. In all these situations methods like Add(T) and Remove(T) are not necessary. This is a clear violation of the ISP: The client should not be forced to depend on methods it does not use.
This is why the interface IReadOnlyList<T> was introduced later with .NET 4.5. This ISP violation became prominent. The .NET developers needed a finer-grained interface to avoid depending on Add(T) and Remove(T) methods they didn’t need. IList<T> needed to be segregated to address this need.
Why is this a problem?
Being coupled with some unneeded behavior is a problem:
- In the optimal scenario, it is a waste. This forces the client to consume precious brain-cycles to consider something he/she doesn’t need.
- In the worst-case scenario, it introduces the risk of errors. Clients may inadvertently misuse additional functionalities, such as attempting to add an element to an array using ICollection<T>.Add().
When an old ISP violation prevents good design forever
It would make sense that IList<T> inherits from IReadOnlyList<T>. A list of items is a read-only list with additional operations like Add(T) and Remove(T). Unfortunately, this is not the case. Since IReadOnlyList<T> appeared later this would have caused some backward compatibility issues.
For example, this class would have been broken if IList<T> was implementing IReadOnlyList<T>. In that case, the property Count would have been defined by IReadOnlyList<T>. And the explicit interface implementation IList<T>.Count would break:
|
1 2 3 4 |
class MyList<T> : IList<T> { int IList<T>.Count { get { return 0; } } // ... } |
A small interface is not necessarily a good abstraction
A single-method interface often makes sense:
- an IExecutor that Execute(),
- an IVisitor that Visit(),
- an IParent that exposes Children { get; }.
Often, such a minimalist interface should be generic. For example, the interface ICloneable available since the inception of .NET is nowadays considered a code smell. When using it the client needs to downcast the cloned Object reference returned to do anything useful with the cloned instance.
|
1 2 3 |
public interface ICloneable { object Clone(); } |
ICloneable has another major drawback: it doesn’t inform the client if the clone operation is deep or shallow. This problem is even more serious than the Object reference downcasting one: it is a real design problem. As we can see a minimalist interface is not necessarily a good abstraction. In this example, the lack of information means ambiguity for the client. This would have been a better design:
|
1 2 3 4 5 6 |
public interface IDeepCloneable<T> { T DeepClone(); } public interface IShallowCloneable<T> { T ShallowClone(); } |
A fat interface is not necessarily a design flaw
A static analysis rule like Avoid too large interfaces can certainly pinpoint most of the ISP violations. A threshold of 10 methods is proposed by default to define what a too-large interface is.
However, as always with code metrics and static analysis, such a rule can also spit some false positives. For example, the .NET interface IConvertible is fat but is valid. Classes that implement it should be convertible to all those primitive types.
For example, we can imagine a Complex number class that implements IConvertible. All to-number methods would return the magnitude of the complex number. ToBoolean() would return false only if the complex is zero. ToString() would return the complex string representation.
ISP and the Liskov Substitution Principle (LSP)
ISP and LSP can be likened to two sides of the same coin:
- ISP is the client perspective: If an interface is too fat probably the client sees some behaviors it doesn’t need.
- LSP is the implementer perspective: If an interface is too fat probably a class that implements it won’t implement all its behaviors. Some behavior will end up throwing something like a NotSupportedException.
Remember the ICollection<T> interface already discussed in the LSP article. This interface forces all its implementers to implement an Add() method. From the Array class perspective, implementing ICollection<T> is a violation of the LSP because the array doesn’t support element adding:
In the same way, many clients will only need a read-only view of consumed collections. ICollection<T> also violates the ISP: it forces those clients to be coupled with Add() / Insert() / Remove() methods they don’t need. The introduction of IReadOnlyCollection<T> solved both ISP and LSP violations.
This example also shows that ISP doesn’t necessarily mean that a class should implement several lightweight interfaces. It is fine to nest interfaces like russian-nesting-dolls. ICollection<T> is a bit fat, it does a lot, read, add, insert, remove, count… But this interface is well-adapted both for classes that are read/write collections and for clients that work with read/write collection. It makes more sense to nest both read/write behaviors into ICollection<T> than to decompose both behaviors into IReadOnlyCollection<T> and a hypothetical IWriteOnlyCollection<T> interface.
How to Detect ISP Violations in C#
You rarely set out to write a fat interface; it grows one convenient method at a time. A few concrete symptoms tell you the Interface Segregation Principle is being violated in your C# code:
- An implementation throws NotImplementedException or NotSupportedException for part of the contract. This is also an LSP violation, which is why the two principles travel together.
- A client references an interface but only ever calls a small subset of its members.
- Adding one method to an interface forces edits in classes that don’t care about that method.
- A single interface mixes unrelated responsibilities, such as reading and writing, or persistence and formatting, in the same contract.
- Static analysis flags the type. The NDepend rule Avoid too large interfaces reports interfaces above a configurable member threshold, so the fat ones surface during code review instead of in production.
Benefits of the Interface Segregation Principle
Splitting fat interfaces into role interfaces pays off in several ways:
- Lower coupling: a client depends only on the members it calls, so unrelated changes don’t ripple into it.
- Fewer runtime surprises: no half-implemented methods throwing exceptions because a class was forced to honor a contract it cannot fulfill.
- Clearer intent: a small role interface documents exactly what a client needs, which makes the code easier to read and easier to mock in unit tests.
- Safer evolution: you extend behavior by adding a new focused interface rather than editing one that is widely implemented.
- Better composability: a class states precisely the roles it plays by implementing several lean interfaces.
Interface Segregation Principle FAQ
What is the Interface Segregation Principle in C#?
The Interface Segregation Principle is the “I” in SOLID. It states that a client should not be forced to depend on methods it does not use. In C# and .NET you apply it by splitting large interfaces into smaller, role-specific ones, for instance consuming IReadOnlyList<T> instead of the full IList<T> when you only need read access.
What problem does the ISP solve?
It removes accidental coupling. When an interface is too fat, every client is exposed to members it never calls. In the best case that wastes attention; in the worst case it invites misuse, such as calling Add() on a fixed-size array.
What is an example of an ISP violation in .NET?
ICollection<T> forces read-only consumers to depend on Add(), Insert() and Remove(). The introduction of IReadOnlyCollection<T> and IReadOnlyList<T> segregated those concerns and fixed the violation.
How is the ISP different from the Single Responsibility Principle?
SRP is about a class having one reason to change. ISP is about an interface exposing only what a given client needs. They reinforce each other: cohesive, single-responsibility types naturally lead to focused interfaces.
Does the ISP mean every interface should have a single method?
No. Segregation is about the client’s needs, not about counting methods. A single-method interface can be a poor abstraction, as ICloneable shows, while a larger interface such as IConvertible can be perfectly valid when every client genuinely uses the whole contract.
Conclusion
The Interface Segregation Principle keeps your C# code honest about what each client really needs. Instead of one fat interface that every consumer drags around, you expose small role interfaces that say exactly what they offer, and nothing more. The .NET Base Class Library learned this the hard way, which is why IReadOnlyList<T> and IReadOnlyCollection<T> now sit alongside their read-write counterparts. As with every SOLID principle, the fastest way to find a leaking abstraction is to write tests: a test is the most demanding client your code has, and it will complain about a fat interface long before production does.



Good way of explaining, and nice article to get information regarding
my presentation focus, which i am going to deliver in school.