đź“„

Interface Segregation Principle (ISP)

Intermediate 2 min read 300 words

Clients should not be forced to depend on interfaces they do not use

Interface Segregation Principle (ISP)

“Clients should not be forced to depend on interfaces they do not use.” — Robert C. Martin

The Interface Segregation Principle states that it’s better to have many small, specific interfaces than one large, general-purpose interface. Classes should only need to implement methods they actually use.

Understanding ISP

A “fat interface” forces implementers to either:

  • Implement methods they don’t need
  • Throw NotImplementedException (violating LSP)
  • Leave methods empty (misleading)

Small, focused interfaces let classes implement only what makes sense for them.

Violation Example: Fat Interface

// BAD: One interface trying to do everything
public interface IWorker
{
    void Work();
    void TakeBreak();
    void Eat();
    void Sleep();
    void Manage();
    void Code();
    void Design();
    void Test();
    void Deploy();
}

// Developer must implement everything
public class Developer : IWorker
{
    public void Work() => Console.WriteLine("Coding...");
    public void TakeBreak() => Console.WriteLine("Break time");
    public void Eat() => Console.WriteLine("Eating lunch");
    public void Sleep() => Console.WriteLine("Sleeping");

    // Developer doesn't manage!
    public void Manage() => throw new NotImplementedException();

    public void Code() => Console.WriteLine("Writing code");
    public void Design() => throw new NotImplementedException(); // Maybe...
    public void Test() => Console.WriteLine("Writing tests");
    public void Deploy() => Console.WriteLine("Deploying to production");
}

// Manager must also implement everything
public class Manager : IWorker
{
    public void Work() => Console.WriteLine("Managing team");
    public void TakeBreak() => Console.WriteLine("Break time");
    public void Eat() => Console.WriteLine("Eating lunch");
    public void Sleep() => Console.WriteLine("Sleeping");
    public void Manage() => Console.WriteLine("Managing projects");

    // Manager doesn't code!
    public void Code() => throw new NotImplementedException();
    public void Design() => throw new NotImplementedException();
    public void Test() => throw new NotImplementedException();
    public void Deploy() => throw new NotImplementedException();
}

Problems:

  • Classes implement methods they don’t use
  • NotImplementedException everywhere
  • Changes to interface affect all implementers
  • Hard to understand what each class actually does

Applying ISP: Segregated Interfaces

// GOOD: Small, focused interfaces
public interface IWorker
{
    void Work();
}

public interface IHuman
{
    void Eat();
    void Sleep();
    void TakeBreak();
}

public interface IManager
{
    void Manage();
    void ConductMeeting();
    void ReviewPerformance();
}

public interface IDeveloper
{
    void Code();
    void Review();
    void Debug();
}

public interface IDesigner
{
    void Design();
    void CreateMockup();
}

public interface ITester
{
    void Test();
    void WriteTestCases();
}

public interface IDevOps
{
    void Deploy();
    void Monitor();
    void Configure();
}
// Developer implements only what makes sense
public class Developer : IWorker, IHuman, IDeveloper
{
    public void Work() => Code();
    public void Code() => Console.WriteLine("Writing clean code");
    public void Review() => Console.WriteLine("Reviewing pull request");
    public void Debug() => Console.WriteLine("Hunting bugs");

    public void Eat() => Console.WriteLine("Lunch break");
    public void Sleep() => Console.WriteLine("Getting rest");
    public void TakeBreak() => Console.WriteLine("Coffee time");
}

// Manager implements management capabilities
public class Manager : IWorker, IHuman, IManager
{
    public void Work() => Manage();
    public void Manage() => Console.WriteLine("Managing team");
    public void ConductMeeting() => Console.WriteLine("Running standup");
    public void ReviewPerformance() => Console.WriteLine("Quarterly review");

    public void Eat() => Console.WriteLine("Lunch meeting");
    public void Sleep() => Console.WriteLine("Getting rest");
    public void TakeBreak() => Console.WriteLine("Checking emails");
}

// Tech Lead combines multiple roles
public class TechLead : IWorker, IHuman, IDeveloper, IManager
{
    public void Work() => Console.WriteLine("Balancing tasks");

    public void Code() => Console.WriteLine("Writing critical code");
    public void Review() => Console.WriteLine("Reviewing architecture");
    public void Debug() => Console.WriteLine("Solving complex issues");

    public void Manage() => Console.WriteLine("Leading the team");
    public void ConductMeeting() => Console.WriteLine("Technical discussion");
    public void ReviewPerformance() => Console.WriteLine("Team review");

    public void Eat() => Console.WriteLine("Lunch");
    public void Sleep() => Console.WriteLine("Sleep");
    public void TakeBreak() => Console.WriteLine("Break");
}

// Full-stack developer with DevOps skills
public class FullStackDevOps : IWorker, IHuman, IDeveloper, IDevOps
{
    public void Work() => Console.WriteLine("Full stack work");

    public void Code() => Console.WriteLine("Frontend and backend");
    public void Review() => Console.WriteLine("Code review");
    public void Debug() => Console.WriteLine("Debugging");

    public void Deploy() => Console.WriteLine("CI/CD pipeline");
    public void Monitor() => Console.WriteLine("Checking metrics");
    public void Configure() => Console.WriteLine("Infrastructure as code");

    public void Eat() => Console.WriteLine("Eating");
    public void Sleep() => Console.WriteLine("Sleeping");
    public void TakeBreak() => Console.WriteLine("Break");
}

Real-World Example: Repository Interfaces

// BAD: Fat repository interface
public interface IRepository<T>
{
    T GetById(int id);
    List<T> GetAll();
    List<T> GetPaged(int page, int size);
    T GetByCode(string code);
    void Add(T entity);
    void Update(T entity);
    void Delete(int id);
    void SoftDelete(int id);
    void Restore(int id);
    void BulkInsert(List<T> entities);
    void BulkUpdate(List<T> entities);
    int Count();
    bool Exists(int id);
}

// Not all entities support all operations!
// Products can be soft-deleted, Orders cannot
// Users have codes, Products don't
// GOOD: Segregated repository interfaces
public interface IReadRepository<T>
{
    Task<T> GetByIdAsync(int id);
    Task<List<T>> GetAllAsync();
    Task<bool> ExistsAsync(int id);
    Task<int> CountAsync();
}

public interface IWriteRepository<T>
{
    Task AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(int id);
}

public interface IPagedRepository<T>
{
    Task<PagedResult<T>> GetPagedAsync(int page, int pageSize);
}

public interface ISoftDeletable<T>
{
    Task SoftDeleteAsync(int id);
    Task RestoreAsync(int id);
    Task<List<T>> GetDeletedAsync();
}

public interface IBulkOperations<T>
{
    Task BulkInsertAsync(List<T> entities);
    Task BulkUpdateAsync(List<T> entities);
}

public interface ICodeLookup<T>
{
    Task<T> GetByCodeAsync(string code);
}
// Product repository - supports soft delete and bulk operations
public class ProductRepository :
    IReadRepository<Product>,
    IWriteRepository<Product>,
    IPagedRepository<Product>,
    ISoftDeletable<Product>,
    IBulkOperations<Product>
{
    // Implements all relevant interfaces
}

// Order repository - no soft delete, no bulk
public class OrderRepository :
    IReadRepository<Order>,
    IWriteRepository<Order>,
    IPagedRepository<Order>
{
    // Only implements what Orders need
}

// User repository - has code lookup
public class UserRepository :
    IReadRepository<User>,
    IWriteRepository<User>,
    ICodeLookup<User>
{
    // Users can be looked up by employee code
}

ISP in Service Design

// BAD: God service interface
public interface IUserService
{
    User GetById(int id);
    User GetByEmail(string email);
    void Create(User user);
    void Update(User user);
    void Delete(int id);
    void ChangePassword(int userId, string newPassword);
    void ResetPassword(string email);
    void SendVerificationEmail(int userId);
    void VerifyEmail(string token);
    void Enable2FA(int userId);
    void Disable2FA(int userId);
    void UpdateProfile(int userId, ProfileData data);
    void UploadAvatar(int userId, byte[] image);
    List<User> Search(string query);
    PagedResult<User> GetPaged(int page, int size);
}
// GOOD: Segregated services
public interface IUserQueryService
{
    Task<User> GetByIdAsync(int id);
    Task<User> GetByEmailAsync(string email);
    Task<List<User>> SearchAsync(string query);
    Task<PagedResult<User>> GetPagedAsync(int page, int size);
}

public interface IUserCommandService
{
    Task CreateAsync(User user);
    Task UpdateAsync(User user);
    Task DeleteAsync(int id);
}

public interface IPasswordService
{
    Task ChangePasswordAsync(int userId, string currentPassword, string newPassword);
    Task ResetPasswordAsync(string email);
}

public interface IEmailVerificationService
{
    Task SendVerificationEmailAsync(int userId);
    Task VerifyEmailAsync(string token);
}

public interface ITwoFactorAuthService
{
    Task EnableAsync(int userId);
    Task DisableAsync(int userId);
    Task<bool> ValidateCodeAsync(int userId, string code);
}

public interface IProfileService
{
    Task UpdateProfileAsync(int userId, ProfileData data);
    Task UploadAvatarAsync(int userId, byte[] image);
    Task<ProfileData> GetProfileAsync(int userId);
}

Benefits of ISP

Benefit Description
Smaller Contracts Interfaces are easier to understand
Flexible Implementation Classes implement only what they need
Easier Testing Mock only relevant interfaces
Better Cohesion Related methods are grouped together
Reduced Coupling Clients depend on minimal interface

Interview Tips

Common Questions:

  • “Explain ISP with an example”
  • “How do you decide when to split an interface?”
  • “What’s the relationship between ISP and SRP?”

Key Points:

  1. Fat interfaces force unnecessary dependencies
  2. Small interfaces are easier to implement and test
  3. ISP relates to SRP - both about focused responsibilities
  4. Use role interfaces instead of header interfaces

Signs of ISP Violation:

  • NotImplementedException in interface implementations
  • Empty method implementations
  • Interfaces with 10+ methods
  • Clients only using a few methods of an interface

Interface Size Guidelines:

  • 1-5 methods: Good
  • 5-10 methods: Consider splitting
  • 10+ methods: Probably needs splitting