Quick Recap & Series Intro
Series Articles
This is Article 4 of 5 in our SOLID series for C# Corner. So far, we've covered Single Responsibility (one reason to change), Open/Closed (extend without modifying), and Liskov Substitution (subclasses that don't break their parent's promises). Today we're tackling the "I" — and if you've ever built an interface that some classes only partially implement, this one's going to feel personal.
If you haven't read the earlier parts yet, it's worth a quick look since these principles build on each other in practice, even though each article stands on its own:
Up next after this one: the Dependency Inversion Principle (DIP) — the final letter in SOLID.
What is the Interface Segregation Principle?
In plain English: no class should be forced to implement methods it doesn't actually use.
If you're designing an interface and find yourself writing methods that only some of your implementing classes will ever need, that's the warning sign. ISP says it's better to have several small, focused interfaces than one large interface that tries to cover every possible use case.
This isn't about being minimalist for its own sake. It's about respecting the people who will implement your interface later — including future-you.
Every method on an interface is a promise. The more unrelated promises you bundle into one contract, the more classes end up faking compliance instead of genuinely fulfilling it.
A General Real-World Example
Picture a universal remote control that tries to do everything — TV, air conditioner, soundbar, ceiling fan, and set-top box — all crammed onto one device with 200 tiny buttons.
Even if all you ever do is turn the TV on and adjust the volume, you're still stuck carrying around buttons for the AC's swing mode and the fan's timer setting every single day, on every remote you own.
Compare that to having:
Each one is small, easy to use, and only contains buttons relevant to the device it controls.
You're never forced to "implement" — or in this case, carry around — functionality you don't need.
That's the entire idea behind ISP, translated from buttons to method signatures.
Why Developers Violate This Principle Without Realizing It
Fat interfaces rarely start out fat.
They usually begin reasonably — an interface with two or three closely related methods. Then a new requirement appears, and instead of creating a new interface, it feels faster to add one more method to the existing one.
After all, most implementing classes are already there, so why create something new?
Repeat that decision ten times over a year, and you end up with an interface that has fifteen methods, where any given implementing class genuinely needs maybe four of them.
The rest get implemented with method bodies that:
Throw exceptions
Return default values
Do nothing
These are quiet admissions that the interface no longer fits.
Another common cause is designing interfaces around what a single class happens to do instead of what each client actually needs.
An interface should be shaped by its consumers, not by convenience.
C# Example 1 — The Office Printer
Before (Violates ISP)
public interface IMultiFunctionDevice
{
void Print(string document);
void Scan(string document);
void Fax(string document);
void Staple(string document);
}
public class BasicPrinter : IMultiFunctionDevice
{
public void Print(string document) =>
Console.WriteLine($"Printing: {document}");
public void Scan(string document) =>
throw new NotSupportedException(
"This printer cannot scan.");
public void Fax(string document) =>
throw new NotSupportedException(
"This printer cannot fax.");
public void Staple(string document) =>
throw new NotSupportedException(
"This printer cannot staple.");
}
BasicPrinter only does one thing well, but the interface forces it to pretend it can do four.
Any code that receives an IMultiFunctionDevice and calls Scan() has no way of knowing, just from the type, whether that call is safe.
After (Follows ISP)
public interface IPrinter
{
void Print(string document);
}
public interface IScanner
{
void Scan(string document);
}
public interface IFax
{
void Fax(string document);
}
public class BasicPrinter : IPrinter
{
public void Print(string document) =>
Console.WriteLine($"Printing: {document}");
}
public class OfficeMultiFunctionDevice :
IPrinter,
IScanner,
IFax
{
public void Print(string document) =>
Console.WriteLine($"Printing: {document}");
public void Scan(string document) =>
Console.WriteLine($"Scanning: {document}");
public void Fax(string document) =>
Console.WriteLine($"Faxing: {document}");
}
What Changed?
Each capability is now its own interface.
BasicPrinter only implements what it genuinely supports.
The office multifunction device implements all relevant interfaces.
Code that only needs printing can depend solely on IPrinter.
This removes the risk of calling unsupported functionality.
C# Example 2 — Smart Home Devices
Before (Violates ISP)
public interface ISmartDevice
{
void TurnOn();
void TurnOff();
void SetTemperature(int degrees);
void PlayMusic(string track);
}
public class SmartBulb : ISmartDevice
{
public void TurnOn() =>
Console.WriteLine("Bulb on");
public void TurnOff() =>
Console.WriteLine("Bulb off");
public void SetTemperature(int degrees) =>
throw new NotSupportedException(
"A bulb can't set temperature.");
public void PlayMusic(string track) =>
throw new NotSupportedException(
"A bulb can't play music.");
}
A smart bulb has no business being asked to play music or set a temperature, but ISmartDevice makes those capabilities part of its contract.
After (Follows ISP)
public interface ISwitchable
{
void TurnOn();
void TurnOff();
}
public interface IThermostat
{
void SetTemperature(int degrees);
}
public interface IMusicPlayer
{
void PlayMusic(string track);
}
public class SmartBulb : ISwitchable
{
public void TurnOn() =>
Console.WriteLine("Bulb on");
public void TurnOff() =>
Console.WriteLine("Bulb off");
}
public class SmartThermostat :
ISwitchable,
IThermostat
{
public void TurnOn() =>
Console.WriteLine("Thermostat on");
public void TurnOff() =>
Console.WriteLine("Thermostat off");
public void SetTemperature(int degrees) =>
Console.WriteLine($"Setting to {degrees}°");
}
public class SmartSpeaker :
ISwitchable,
IMusicPlayer
{
public void TurnOn() =>
Console.WriteLine("Speaker on");
public void TurnOff() =>
Console.WriteLine("Speaker off");
public void PlayMusic(string track) =>
Console.WriteLine($"Playing {track}");
}
What Changed?
Every device implements only the interfaces it actually needs.
SmartBulb no longer pretends it supports music playback or temperature control.
New devices can be created by combining focused interfaces instead of modifying a large interface.
Applying This in an HRMS Project
Let's bring this into an HRMS application, specifically the Leave Management module.
A common first design wraps every leave-related operation into a single interface.
Before (Violates ISP)
public interface IEmployeeService
{
void ApplyLeave(LeaveRequest request);
void ApproveLeave(int requestId);
void RejectLeave(int requestId);
decimal CalculatePayroll(int employeeId);
void GenerateTaxReport(int employeeId);
}
This interface is consumed by very different clients:
Each client depends on methods it never uses.
After (Follows ISP)
public interface IEmployeeSelfService
{
void ApplyLeave(LeaveRequest request);
}
public interface ILeaveApprover
{
void ApproveLeave(int requestId);
void RejectLeave(int requestId);
}
public interface IPayrollOperations
{
decimal CalculatePayroll(int employeeId);
void GenerateTaxReport(int employeeId);
}
Benefits
Employee portal depends only on IEmployeeSelfService.
Manager dashboard depends only on ILeaveApprover.
Payroll jobs depend only on IPayrollOperations.
Security and access control become easier to manage.
Reduced coupling between unrelated features.
A concrete EmployeeService can still implement all three interfaces internally while exposing only the required contract to each consumer.
Common Mistakes and Pitfalls
Adding Methods to Existing Interfaces
A new method may seem useful for most implementations, but if even one implementation doesn't need it, you may be introducing an ISP violation.
Designing Around Implementations
Interfaces should be designed around client requirements, not around a single concrete class.
Ignoring Warning Signs
Watch out for methods that:
These are often indicators that an interface should be split.
Over-Segregating Interfaces
It's possible to go too far.
Creating dozens of single-method interfaces without a real need can add unnecessary complexity.
The goal is not the maximum number of interfaces. The goal is interfaces aligned with real client needs.
Quick Checklist
Ask yourself:
Does every implementing class use every method on the interface?
Are there methods that only throw exceptions or do nothing?
Can you describe the interface in one short sentence?
Does the interface represent one clear responsibility?
Can a client depend only on the functionality it needs?
When you last added a method, did every existing implementation actually need it?
Conclusion
The Interface Segregation Principle is a simple discipline with an outsized payoff.
Smaller, purpose-built interfaces lead to:
Instead of forcing classes to implement behavior they don't need, ISP encourages creating focused contracts that match real-world responsibilities.
In practice, this results in cleaner, more reliable, and more extensible software systems.