Design Patterns & Practices  

Interface Segregation Principle (ISP) in C#: Keep Interfaces Lean

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