SOLID Design in C#: The Interface Segregation Principle (ISP) with Examples - NDepend Blog (original) (raw)

May 25, 2026 9 minutes read

The Interface Segregation Principle

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

SOLID Principles - Interface Segregation Principle

Before delving into the Interface Segregation Principle, let’s take a quick look at how it stands in the SOLID design principles:

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:

| | 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:

| | 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:

| | 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:

| | 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. 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 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 needed to be segregated to address this need.

Why is this a problem?

Being coupled with some unneeded behavior is a problem:

When an old ISP violation prevents good design forever

It would make sense that IList inherits from IReadOnlyList. 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 appeared later this would have caused some backward compatibility issues.

For example, this class would have been broken if IList was implementing IReadOnlyList. In that case, the property Count would have been defined by IReadOnlyList. And the explicit interface implementation IList.Count would break:

| | 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:

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.

| | 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:

| | 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:

Remember the ICollection 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 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 also violates the ISP: it forces those clients to be coupled with Add() / Insert() / Remove() methods they don’t need. The introduction of IReadOnlyCollection 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 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 than to decompose both behaviors into IReadOnlyCollection and a hypothetical IWriteOnlyCollection 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:

Benefits of the Interface Segregation Principle

Splitting fat interfaces into role interfaces pays off in several ways:

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 instead of the full IList 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 forces read-only consumers to depend on Add(), Insert() and Remove(). The introduction of IReadOnlyCollection and IReadOnlyList 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 and IReadOnlyCollection 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.

This article is brought to you by the team behind NDepend — a proven .NET static analysis tool for improving code maintainability, security, and overall quality. Whether you’re modernizing a legacy .NET application or starting fresh in C#, get started with your free full-featured trial today!