Interface Segregation Principle in C#: Focused Interfaces That Scale
The interface segregation principle c# is the fourth letter in SOLID -- and one of the most misunderstood. The definition is deceptively simple: clients should not be forced to depend on interfaces they do not use. Sounds reasonable on paper. In practice, it gets violated constantly. You end up with bloated interfaces that force implementors to throw NotImplementedException or leave methods empty. That is often a design smell. In framework scaffolding, legacy compatibility layers, or placeholder implementations, context matters. This article walks you through what ISP looks like in the real world, how to fix fat interfaces, and how the principle plays out with modern .NET 10 dependency injection.
What Is the interface segregation principle c#?
ISP was articulated by Robert C. Martin as part of the SOLID design principles. The rule is simple but consequential: no class should be forced to implement methods that do not apply to it.
When you design one large interface that covers multiple responsibilities, every class that implements it carries the full weight -- even the parts it does not need. The result is brittle code, fake implementations, and interfaces that lie about what a type can actually do.
The principle pushes you toward role interfaces -- small, focused interfaces that represent a single capability. A class implements only the interfaces relevant to it. A consumer depends only on the interface it actually uses.
This is closely tied to the Single Responsibility Principle. Just as a class should have one reason to change, an interface should represent one coherent role. When an interface grows beyond a single responsibility, it starts forcing unrelated concerns onto every implementor.
The "Fat Interface" Smell
Fat interfaces grow organically. Someone creates a small, sensible interface. A new feature arrives. The path of least resistance is to add the new method to the existing interface. Repeat that ten times, and you have a bloated contract that no single class should ever implement in full.
You're looking at a fat interface when:
- Classes throw
NotImplementedExceptionto satisfy the contract - You need to mock six methods just to test one
- Two implementing classes share an interface but have almost nothing in common
- The interface name is vague -- something like
IServiceorIHelpercovering unrelated concerns
These are signals that the interface has grown beyond a single cohesive responsibility. Recognizing this smell is the first step to applying the interface segregation principle c# effectively.
Before: The Fat IWorker Interface
Here is a classic example. A single IWorker interface that tries to model every type of employee:
// ❌ Fat interface -- forces all implementors to carry every method
public interface IWorker
{
void DoWork();
void ManageTeam();
void GenerateReport();
void SetSalary(decimal amount);
}
This might look reasonable at first glance. But what happens when you implement it for a regular developer?
public class Developer : IWorker
{
public void DoWork() => Console.WriteLine("Writing code...");
// These make no sense for a Developer
public void ManageTeam() =>
throw new NotImplementedException("Developers don't manage teams.");
public void GenerateReport() =>
throw new NotImplementedException("Developers don't generate reports.");
public void SetSalary(decimal amount) =>
throw new NotImplementedException("Developers don't set salaries.");
}
Three NotImplementedException throws. The Developer class is being punished for responsibilities that belong to someone else entirely. That is the fat interface smell in its most visible form. The contract is lying -- Developer says it can manage teams and set salaries, but it cannot.
After: Split into Role Interfaces
The fix is to break IWorker into smaller, focused role interfaces -- each representing a single capability:
// ✅ Focused role interfaces -- each represents one capability
public interface IWorker
{
void DoWork();
}
public interface IManager
{
void ManageTeam();
void SetSalary(decimal amount);
}
public interface IReporter
{
void GenerateReport();
}
Now implementing classes are completely honest about what they can do:
public sealed class Developer : IWorker
{
public void DoWork() => Console.WriteLine("Writing code...");
}
public sealed class Manager : IWorker, IManager
{
public void DoWork() => Console.WriteLine("Reviewing pull requests...");
public void ManageTeam() => Console.WriteLine("Running 1:1s...");
public void SetSalary(decimal amount) => Console.WriteLine($"Setting salary: {amount:C}");
}
public sealed class ReportingManager : IWorker, IManager, IReporter
{
public void DoWork() => Console.WriteLine("Reviewing architecture...");
public void ManageTeam() => Console.WriteLine("Leading the team...");
public void SetSalary(decimal amount) => Console.WriteLine($"Setting salary: {amount:C}");
public void GenerateReport() => Console.WriteLine("Generating quarterly report...");
}
No fake implementations. No NotImplementedException. Every method on every class means something real.
Notice that ReportingManager plays three roles simultaneously -- worker, manager, and reporter. The role interfaces let the type system express that precisely without forcing a monolithic hierarchy or a single fat base interface. This is the interface segregation principle c# in action -- each type carries only what it can honestly deliver.
Role Interfaces: The Right Mental Model
The interface segregation principle c# is best understood through role interfaces. The phrase "role interface" is a useful framing device.Instead of designing interfaces around what a class has, design them around what a class can do in a specific context.
A Manager plays two roles: it works and it manages. A Developer plays one role: it works. An AnalyticsReporter might only implement IReporter. Interface names reflect a capability, not a class name. This naming discipline matters -- it keeps the intent of each interface clear.
This model scales well. Adding a new capability -- say, INotifiable for push notifications -- means defining a new interface and implementing it in classes that need it. Existing classes do not change unless they need that capability. The Open/Closed Principle and ISP work together naturally here.
This thinking appears throughout classic design patterns. The Mediator Design Pattern defines focused contracts between collaborating components -- each participant depends on a minimal interface, not the full mediator object. The Template Method Design Pattern similarly decomposes behavior into focused abstract steps rather than one monolithic method -- each step represents a single role in the algorithm.
The Iterator Design Pattern is a textbook ISP example from the .NET standard library. IEnumerable<T> exposes only what iteration requires. It does not expose sorting, searching, or modification. It does one thing: enable traversal.
ISP and Dependency Injection in .NET 10
The interface segregation principle c# becomes especially powerful when paired with the .NET 10 dependency injection container. Focused interfaces let you inject exactly what a consumer needs -- nothing more, nothing less. Constructors become honest declarations of intent.
Here is how you register multiple focused interfaces and their implementations:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// Register each focused interface separately
builder.Services.AddScoped<IWorker, Developer>();
builder.Services.AddScoped<IManager, Manager>();
builder.Services.AddScoped<IReporter, ReportingManager>();
var app = builder.Build();
await app.RunAsync();
And here is how consumers declare exactly what they need through constructor injection:
// Only needs IWorker -- knows nothing about managers or reporters
public sealed class WorkScheduler(IWorker worker)
{
public void Schedule() => worker.DoWork();
}
// Only needs IManager -- knows nothing about workers or reporters
public sealed class HRService(IManager manager)
{
public void UpdateSalary(decimal newSalary) => manager.SetSalary(newSalary);
public void RunTeamMeeting() => manager.ManageTeam();
}
// Only needs IReporter
public sealed class AnalyticsService(IReporter reporter, ILogger<AnalyticsService> logger)
{
public void RunReport()
{
logger.LogInformation("Starting report generation...");
reporter.GenerateReport();
logger.LogInformation("Report generation complete.");
}
}
WorkScheduler is completely unaware that managers and reporters exist. HRService knows nothing about reporting. Each class depends on the minimum it needs. Constructors are honest. Mocks in tests are lean.
When a constructor is taking in a fat interface but only using two of its ten methods, ISP is being violated -- and the DI registration is hiding it. Small interfaces surface that problem immediately.
If you want to understand how the DI container actually wires these up at runtime -- what happens under the hood when it resolves constructor parameters -- the article on how dependency injection containers use reflection internally explains the mechanics in depth.
Focused interfaces help even outside of DI containers -- the clarity benefits apply whether you're using dependency injection or not.
ISP vs the Facade Pattern
There is an interesting tension between ISP and the Facade Design Pattern. ISP says: break big interfaces into small ones. A Facade says: give external consumers a simplified, unified entry point. Are they contradictory?
Not at all. They solve different problems at different architectural layers.
ISP governs the internal design of your abstractions -- keep interfaces small so implementors and consumers are not burdened. A Facade operates at the boundary between subsystems -- it presents a simplified, cohesive surface to external callers while internally delegating to multiple focused interfaces.
The Facade does not violate ISP. It uses focused interfaces internally and presents a simplified surface externally. ISP and Facade are complementary. Think of the Facade as the integration layer that assembles small ISP-compliant pieces into a larger, usable whole for outside callers.
The same logic applies to the Proxy Design Pattern. A proxy wraps a focused interface -- it does not bloat the contract. Caching proxies, logging proxies, and authorization proxies add behavior without widening the surface the consumer depends on.
ISP in Repository Design
The interface segregation principle c# shows up in concrete ways when designing repository patterns. A typical "full" repository interface looks like this:
// ❌ Fat repository -- forces all consumers to depend on every operation
public interface IRepository<T>
{
T? GetById(int id);
IEnumerable<T> GetAll();
void Add(T entity);
void Update(T entity);
void Delete(int id);
IEnumerable<T> Search(string query);
void BulkInsert(IEnumerable<T> entities);
}
Read-only services only need GetById and GetAll. A background job might only need BulkInsert. A search feature needs Search. If every consumer depends on the full IRepository<T>, you are violating ISP and making tests harder than they need to be.
Split into focused contracts:
// ✅ Focused repository interfaces
public interface IReadRepository<T>
{
T? GetById(int id);
IEnumerable<T> GetAll();
}
public interface IWriteRepository<T>
{
void Add(T entity);
void Update(T entity);
void Delete(int id);
}
public interface ISearchRepository<T>
{
IEnumerable<T> Search(string query);
}
public interface IBulkRepository<T>
{
void BulkInsert(IEnumerable<T> entities);
}
Now inject only what each consumer needs. A read-only query service takes IReadRepository<T>. A search feature takes ISearchRepository<T>. Tests require only the interface methods the class under test actually calls.
Many teams intentionally avoid generic repositories with EF Core altogether -- this example illustrates the ISP principle, not a prescribed repository shape.
This pattern pairs naturally with LINQ in C# -- LINQ expressions map cleanly onto focused query interfaces like IReadRepository<T> and ISearchRepository<T>, keeping query composition in the right layer.
Practical Guidelines: When to Split vs When to Stay Unified
The interface segregation principle c# does not mandate one method per interface. That would be over-engineering. Here are practical guidelines for deciding when to split.
Split when:
- Two or more implementors need different subsets of the methods
- Consumers depend on the interface but only call a fraction of its methods
- Methods represent clearly distinct capabilities (reading vs writing, managing vs reporting)
- Test setup is painful because of unused method stubs
- A class has to throw
NotImplementedExceptionfor any method
Keep unified when:
- All methods are cohesive and always used together
- The interface has two or three methods that represent one clear capability
- There is a single implementor and splitting adds no architectural clarity
- You would end up with a stream of single-method interfaces that add noise without value
The goal is not to minimize interface size for its own sake. The goal is to eliminate forced dependencies. If no class is burdened by methods it does not use, ISP is satisfied -- regardless of interface size.
Interface splits should follow real consumer needs, not interface-size anxiety -- splitting for its own sake can fragment cohesion.
In modular monolith architectures in C#, ISP is particularly important at module boundaries. Each module should expose focused interfaces to other modules -- only what those modules genuinely need. A module that exposes a single giant service interface creates tight coupling across boundaries and undermines the entire point of modularity.
FAQ
What is the interface segregation principle in C#?
The interface segregation principle c# (ISP) states that clients should not be forced to depend on interfaces they do not use. In C#, this means designing small, focused interfaces rather than one large interface covering multiple responsibilities. Each class implements only the interfaces relevant to its role. The result is cleaner code, leaner mocks, and implementors that are honest about what they can do.
Why do fat interfaces lead to NotImplementedException?
When a class is forced to implement an interface that includes methods outside its domain, it has no meaningful implementation to provide. The only option is to throw NotImplementedException or return a meaningless default. This is a design signal -- the interface is too broad for this implementor. ISP eliminates that problem by ensuring each interface matches the actual capabilities of those that implement it.
How does ISP improve unit testing?
Focused interfaces make mocking dramatically easier. When an interface has only the methods your class under test actually uses, you only need to configure those methods in your mock. Fat interfaces force you to stub out irrelevant methods just to satisfy the mock framework. ISP keeps test setup lean, tests easier to read, and test failures easier to diagnose.
Does ISP mean every method should have its own interface?
No. One method per interface is often over-engineering. ISP is about removing forced dependencies, not minimizing interface size at all costs. If multiple methods are always used together and represent a cohesive capability, keeping them in one interface is perfectly correct. Apply ISP when the split removes a real burden from implementors or consumers -- not just to reduce method count.
How does ISP interact with dependency injection in .NET 10?
ISP and .NET 10 DI work together naturally. Focused interfaces let you register each capability separately with AddScoped, AddSingleton, or AddTransient and inject only what a consumer needs. This leads to honest, minimal constructors. It also makes it immediately visible when a class is taking on too much -- the constructor will be pulling in too many focused interfaces. ISP and DI together act as a complexity detector.
What is a "role interface"?
A role interface describes a single capability or role an object can play -- rather than defining all the methods a class has. IManager, IWorker, and IReporter are role interfaces. They let objects play multiple roles without inheriting from a monolithic base class. Naming interfaces after roles rather than classes clarifies the intent of each interface and makes the type system more expressive.
When should I NOT split an interface?
Avoid splitting when all methods represent a cohesive single capability that is always used together, when there is only one consumer, or when splitting would produce trivially obvious single-method interfaces that add noise without architectural value. Splitting should remove a real burden -- not just reduce a number. If you are splitting because it "feels right" but no class benefits from the split, stop. ISP is a practical tool, not a counting exercise.
Conclusion
The interface segregation principle c# is about designing contracts that are honest. Honest about what a type can do. Honest about what a consumer needs. The moment a class starts throwing NotImplementedException or a consumer injects a fat interface it barely uses, ISP is being violated.
Keep your interfaces focused. Name them after roles. Split them when different implementors or consumers need different subsets. In .NET 10, ISP and dependency injection amplify each other -- small interfaces lead to small constructors, clear dependencies, and code that is genuinely easier to change and test.
Fat interfaces are a form of technical debt. The sooner you pay it down, the cleaner the codebase stays.

