Summary
This article explores the Interface Segregation Principle (ISP)βclients should not be forced to depend on interfaces they don't use. You'll learn how to design focused, cohesive interfaces instead of bloated "kitchen sink" interfaces. Through practical C# examples, we'll refactor fat interfaces into lean, purpose-specific contracts that make code more maintainable, testable, and flexible.
Prerequisites
Strong understanding of C# interfaces
Familiarity with implementing and consuming interfaces
Knowledge of previous SOLID principles helpful
Basic understanding of dependency injection
The Interface Segregation Principle states: No client should be forced to depend on methods it does not use.
In simpler terms: many small, focused interfaces are better than one large, general-purpose interface. Let's see why.
The Worker Problem
You're building a workforce management system. You create an interface for all workers:
public interface IWorker
{
void Work();
void Eat();
void Sleep();
void TakeBreak();
}
public class HumanWorker : IWorker
{
public void Work()
{
Console.WriteLine("Human working...");
}
public void Eat()
{
Console.WriteLine("Human eating lunch...");
}
public void Sleep()
{
Console.WriteLine("Human sleeping...");
}
public void TakeBreak()
{
Console.WriteLine("Human taking a break...");
}
}
Works fine. Then you add robots:
public class Robot : IWorker
{
public void Work()
{
Console.WriteLine("Robot working 24/7...");
}
public void Eat()
{
throw new NotImplementedException("Robots don't eat");
}
public void Sleep()
{
throw new NotImplementedException("Robots don't sleep");
}
public void TakeBreak()
{
throw new NotImplementedException("Robots don't take breaks");
}
}
This violates ISP. Robot
is forced to implement methods it doesn't use. Worse, these methods throw exceptions or do nothing.
Refactoring with ISP
Let's segregate the interface:
public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public interface ISleepable
{
void Sleep();
}
public interface IBreakable
{
void TakeBreak();
}
public class HumanWorker : IWorkable, IEatable, ISleepable, IBreakable
{
public void Work()
{
Console.WriteLine("Human working...");
}
public void Eat()
{
Console.WriteLine("Human eating...");
}
public void Sleep()
{
Console.WriteLine("Human sleeping...");
}
public void TakeBreak()
{
Console.WriteLine("Human on break...");
}
}
public class Robot : IWorkable
{
public void Work()
{
Console.WriteLine("Robot working continuously...");
}
}
Now Robot
only implements what it needs. No dummy implementations, no exceptions.
The Printer Problem
Here's another common violation. You're managing office devices:
public interface IMultiFunctionDevice
{
void Print(Document document);
void Scan(Document document);
void Fax(Document document);
void Copy(Document document);
void Email(Document document);
}
public class AllInOnePrinter : IMultiFunctionDevice
{
public void Print(Document document)
{
Console.WriteLine("Printing...");
}
public void Scan(Document document)
{
Console.WriteLine("Scanning...");
}
public void Fax(Document document)
{
Console.WriteLine("Faxing...");
}
public void Copy(Document document)
{
Console.WriteLine("Copying...");
}
public void Email(Document document)
{
Console.WriteLine("Emailing...");
}
}
Fine for all-in-one devices. But what about a simple printer?
public class SimplePrinter : IMultiFunctionDevice
{
public void Print(Document document)
{
Console.WriteLine("Printing...");
}
public void Scan(Document document)
{
throw new NotSupportedException("This printer cannot scan");
}
public void Fax(Document document)
{
throw new NotSupportedException("This printer cannot fax");
}
public void Copy(Document document)
{
throw new NotSupportedException("This printer cannot copy");
}
public void Email(Document document)
{
throw new NotSupportedException("This printer cannot email");
}
}
SimplePrinter
is forced to implement methods it doesn't support. ISP violation.
Segregated Interfaces
public interface IPrinter
{
void Print(Document document);
}
public interface IScanner
{
void Scan(Document document);
}
public interface IFax
{
void Fax(Document document);
}
public interface ICopier
{
void Copy(Document document);
}
public interface IEmailer
{
void Email(Document document);
}
public class SimplePrinter : IPrinter
{
public void Print(Document document)
{
Console.WriteLine("Printing...");
}
}
public class AllInOnePrinter : IPrinter, IScanner, IFax, ICopier, IEmailer
{
public void Print(Document document)
{
Console.WriteLine("Printing...");
}
public void Scan(Document document)
{
Console.WriteLine("Scanning...");
}
public void Fax(Document document)
{
Console.WriteLine("Faxing...");
}
public void Copy(Document document)
{
Console.WriteLine("Copying...");
}
public void Email(Document document)
{
Console.WriteLine("Emailing...");
}
}
public class PrinterScanner : IPrinter, IScanner
{
public void Print(Document document)
{
Console.WriteLine("Printing...");
}
public void Scan(Document document)
{
Console.WriteLine("Scanning...");
}
}
Now each device implements only the interfaces it supports. Clean, focused, and no exceptions.
The Repository Anti-Pattern
Consider a generic repository:
public interface IRepository
{
T GetById(int id);
IEnumerable GetAll();
void Add(T entity);
void Update(T entity);
void Delete(int id);
void BulkInsert(IEnumerable entities);
void BulkDelete(IEnumerable ids);
IEnumerable Search(string query);
void ExecuteStoredProcedure(string procedureName, params object[] parameters);
}
Looks comprehensive. But what if you have a read-only view?
public class ReportRepository : IRepository
{
public Report GetById(int id)
{
// Implementation
}
public IEnumerable GetAll()
{
// Implementation
}
public IEnumerable Search(string query)
{
// Implementation
}
// Reports are read-only, so these shouldn't exist
public void Add(Report entity)
{
throw new NotSupportedException();
}
public void Update(Report entity)
{
throw new NotSupportedException();
}
public void Delete(int id)
{
throw new NotSupportedException();
}
public void BulkInsert(IEnumerable entities)
{
throw new NotSupportedException();
}
public void BulkDelete(IEnumerable ids)
{
throw new NotSupportedException();
}
public void ExecuteStoredProcedure(string procedureName, params object[] parameters)
{
throw new NotSupportedException();
}
}
Half the interface is useless for reports. Classic ISP violation.
Proper Segregation
public interface IReadRepository
{
T GetById(int id);
IEnumerable GetAll();
IEnumerable Search(string query);
}
public interface IWriteRepository
{
void Add(T entity);
void Update(T entity);
void Delete(int id);
}
public interface IBulkRepository
{
void BulkInsert(IEnumerable entities);
void BulkDelete(IEnumerable ids);
}
public interface IStoredProcedureRepository
{
void ExecuteStoredProcedure(string procedureName, params object[] parameters);
}
// Full repository
public class ProductRepository :
IReadRepository,
IWriteRepository,
IBulkRepository
{
// Implement all operations
}
// Read-only repository
public class ReportRepository : IReadRepository
{
public Report GetById(int id)
{
// Implementation
}
public IEnumerable GetAll()
{
// Implementation
}
public IEnumerable Search(string query)
{
// Implementation
}
}
Now consumers depend only on what they need. Code reading reports doesn't see write operations.
The Authentication Service Problem
You create a monolithic auth interface:
public interface IAuthenticationService
{
bool Login(string username, string password);
void Logout(string sessionId);
bool ResetPassword(string email);
bool ChangePassword(string oldPassword, string newPassword);
bool ValidateToken(string token);
string GenerateToken(User user);
bool IsAuthorized(User user, string permission);
void SendPasswordResetEmail(string email);
void LogAuthenticationAttempt(string username, bool success);
User GetCurrentUser();
}
Different parts of your application need different subsets:
Login page needs Login
and SendPasswordResetEmail
API middleware needs ValidateToken
and IsAuthorized
User settings page needs ChangePassword
But all of them are forced to depend on the entire interface.
Breaking It Down
public interface ILoginService
{
bool Login(string username, string password);
void Logout(string sessionId);
}
public interface IPasswordService
{
bool ResetPassword(string email);
bool ChangePassword(string oldPassword, string newPassword);
void SendPasswordResetEmail(string email);
}
public interface ITokenService
{
bool ValidateToken(string token);
string GenerateToken(User user);
}
public interface IAuthorizationService
{
bool IsAuthorized(User user, string permission);
}
public interface IUserService
{
User GetCurrentUser();
}
public interface IAuditService
{
void LogAuthenticationAttempt(string username, bool success);
}
Now each component depends only on what it needs:
public class LoginController
{
private readonly ILoginService _loginService;
private readonly IPasswordService _passwordService;
public LoginController(ILoginService loginService, IPasswordService passwordService)
{
_loginService = loginService;
_passwordService = passwordService;
}
}
public class ApiAuthenticationMiddleware
{
private readonly ITokenService _tokenService;
private readonly IAuthorizationService _authService;
public ApiAuthenticationMiddleware(ITokenService tokenService, IAuthorizationService authService)
{
_tokenService = tokenService;
_authService = authService;
}
}
ISP and Dependency Management
ISP has a huge benefit: it reduces coupling. When interfaces are small, changes affect fewer clients:
// Fat interface - change affects all consumers
public interface IOrderService
{
void CreateOrder(Order order);
void CancelOrder(int orderId);
void ShipOrder(int orderId);
decimal CalculateTotal(Order order);
bool ValidateOrder(Order order);
}
// Segregated - changes are isolated
public interface IOrderCreation
{
void CreateOrder(Order order);
}
public interface IOrderCancellation
{
void CancelOrder(int orderId);
}
public interface IOrderShipping
{
void ShipOrder(int orderId);
}
public interface IOrderCalculation
{
decimal CalculateTotal(Order order);
}
public interface IOrderValidation
{
bool ValidateOrder(Order order);
}
If shipping logic changes, only classes depending on IOrderShipping
are affected. With the fat interface, every consumer must be reviewed.
How to Identify ISP Violations
Watch for these warning signs:
1. NotImplementedException
public void SomeMethod()
{
throw new NotImplementedException();
}
If implementations throw exceptions, the interface is too broad.
2. Empty Implementations
public void SomeMethod()
{
// Do nothing
}
Empty methods signal the interface includes unnecessary members.
3. Large Interfaces (More Than 5-7 Methods)
If your interface has dozens of methods, it's probably doing too much.
4. Clients Using Partial Interfaces
// Only uses 2 of 10 interface methods
var service = new MyService();
service.Method1();
service.Method2();
// Other 8 methods never called
ISP and Other SOLID Principles
ISP + SRP: Small interfaces naturally have single responsibilities. When you segregate interfaces by concern, each has one reason to change.
ISP + OCP: Focused interfaces make extending behavior easier. You add new interfaces rather than modifying existing ones.
ISP + LSP: Small interfaces are easier to substitute correctly. Fewer methods mean fewer behavioral contracts to honor.
When NOT to Segregate
Don't go overboard:
Cohesive operations β If methods are tightly related and always used together, keep them in one interface
Simple interfaces β An interface with 2-3 closely related methods doesn't need segregation
Stable interfaces β If an interface hasn't changed in years and all implementations use all methods, leave it alone
Conclusion
The Interface Segregation Principle keeps interfaces focused and purposeful. By creating many small interfaces instead of few large ones, you reduce coupling, improve flexibility, and make code easier to maintain.
When designing interfaces, think about who will use them and for what purpose. Don't create "one interface to rule them all." Instead, create specific contracts for specific needs.
If you find yourself implementing methods with throw new NotImplementedException()
, step back. Your interface is too broad, and segregation is needed.
In the final article of this series, we'll explore the Dependency Inversion Principle and learn how to decouple code through abstraction.
Key Takeaways
β
Small interfaces β Many focused interfaces beat one large interface
β
No forced methods β Clients shouldn't implement methods they don't use
β
Role-based interfaces β Design interfaces around specific roles or capabilities
β
Reduced coupling β Small interfaces mean changes affect fewer consumers
β
Spot violations β NotImplementedException and empty methods are red flags
β
Easier testing β Mocking small interfaces is simpler than large ones
β
Use judgment β Don't over-segregate; balance granularity with practicality
β
Complements SRP β ISP for interfaces is like SRP for classes
What bloated interfaces have you encountered? Share your refactoring stories below!
References and Further Reading