Appearance
Interface Segregation Principle (ISP)
SOLID Series — This article is Part 4 of 5. See also: SRP · OCP · LSP · DIP
Introduction
The Interface Segregation Principle, introduced by Robert C. Martin, states that clients should not be forced to depend on interfaces they do not use. A "fat" interface that bundles unrelated methods forces every implementing class to provide stub or no-op implementations for methods it does not need, polluting the codebase with empty or misleading implementations. ISP solves this by splitting large interfaces into smaller, role-specific ones so that each client depends only on the methods it actually uses.
Core Concept
ISP is, in essence, the Single Responsibility Principle applied at the interface level. Just as a class should have only one reason to change, an interface should represent only one role or capability. When an interface grows to cover multiple distinct concerns, it becomes a burden to every implementor and every caller. The fix is to split the interface along its natural role boundaries and let each class implement only the roles relevant to it.
A useful heuristic: if an implementing class has any method that throws UnsupportedOperationException or returns a stub value like null, 0, or "" just to satisfy an interface contract it cannot fulfill, that interface almost certainly violates ISP.
Anti-Pattern — The Fat Worker Interface
Imagine a workforce management system that defines a single Worker interface with every method a worker might need.
java
// ANTI-PATTERN: Fat Worker interface — bundles unrelated concerns
public interface Worker {
void work();
void eat();
void sleep();
void attendMeeting();
void generateReport();
void takeVacation();
}Now consider a Robot that also needs to implement this interface because it participates in the workflow:
java
// ANTI-PATTERN: Robot forced to implement methods that do not apply to it
public class Robot implements Worker {
@Override
public void work() {
System.out.println("Robot is executing assigned task...");
}
@Override
public void eat() {
// Robots don't eat — forced to provide a meaningless stub
throw new UnsupportedOperationException("Robots do not eat");
}
@Override
public void sleep() {
// Robots don't sleep — this is a lie about Robot's capabilities
throw new UnsupportedOperationException("Robots do not sleep");
}
@Override
public void attendMeeting() {
// Robots don't attend meetings — or do they? Ambiguous
throw new UnsupportedOperationException("Robots do not attend meetings");
}
@Override
public void generateReport() {
// Maybe a Robot can generate reports? But it wasn't designed to...
throw new UnsupportedOperationException("Not implemented");
}
@Override
public void takeVacation() {
// Definitely not applicable to a robot
throw new UnsupportedOperationException("Robots do not take vacations");
}
}Problems:
Robotmust implement six methods but only truly supports one.- Any code that calls
worker.eat()on aRobotwill throw at runtime — an LSP violation enabled by the fat interface. - Adding a new method to
Worker(sayattendTraining()) forces changes toRobot,HumanWorker,ContractWorker, and every other implementor, even those that don't care about training.
Correct Implementation — Segregated Role Interfaces
Split the fat Worker interface into focused, role-based interfaces. Each interface represents one capability.
java
// CORRECT: Role-specific interfaces — each has a single concern
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public interface MeetingAttendable {
void attendMeeting();
}
public interface Reportable {
void generateReport();
}
public interface VacationEligible {
void takeVacation();
}java
// CORRECT: HumanWorker implements all relevant interfaces
public class HumanWorker implements Workable, Eatable, Sleepable,
MeetingAttendable, Reportable, VacationEligible {
private final String name;
public HumanWorker(String name) {
this.name = name;
}
@Override
public void work() {
System.out.println(name + " is working on assigned tasks");
}
@Override
public void eat() {
System.out.println(name + " is having lunch");
}
@Override
public void sleep() {
System.out.println(name + " is resting");
}
@Override
public void attendMeeting() {
System.out.println(name + " is in the weekly standup");
}
@Override
public void generateReport() {
System.out.println(name + " is generating the weekly status report");
}
@Override
public void takeVacation() {
System.out.println(name + " is on vacation");
}
}
// CORRECT: Robot implements only what applies — no stubs, no UnsupportedOperationException
public class Robot implements Workable, Reportable {
private final String robotId;
public Robot(String robotId) {
this.robotId = robotId;
}
@Override
public void work() {
System.out.println("Robot " + robotId + " is executing automated task");
}
@Override
public void generateReport() {
System.out.println("Robot " + robotId + " generated performance telemetry report");
}
// No eat(), no sleep(), no attendMeeting() — these methods simply do not exist
// on Robot's type. Callers that depend on Workable will never accidentally call eat().
}java
// CORRECT: Each client depends only on the interface it needs
public class WorkScheduler {
// Only cares that workers can work
public void scheduleWork(List<Workable> workers) {
workers.forEach(Workable::work);
}
}
public class MealBreakManager {
// Only humans (and Eatable implementors) can take meal breaks
public void scheduleMealBreaks(List<Eatable> eaters) {
eaters.forEach(Eatable::eat);
}
}
public class ReportingDashboard {
// Both robots and humans can generate reports
public void collectAllReports(List<Reportable> reporters) {
reporters.forEach(Reportable::generateReport);
}
}
// Main — demonstrating narrowly typed client dependencies
public class Main {
public static void main(String[] args) {
HumanWorker alice = new HumanWorker("Alice");
Robot r2d2 = new Robot("R2-D2");
WorkScheduler scheduler = new WorkScheduler();
scheduler.scheduleWork(List.of(alice, r2d2)); // Both work
MealBreakManager mealManager = new MealBreakManager();
mealManager.scheduleMealBreaks(List.of(alice)); // Only Alice — R2-D2 cannot be passed here
ReportingDashboard dashboard = new ReportingDashboard();
dashboard.collectAllReports(List.of(alice, r2d2)); // Both report
}
}Repository Interface Segregation
ISP applies powerfully to data access layers. A single CrudRepository<T> interface forces read-only consumers (like reporting services) to depend on write and delete operations they should never call.
java
// ANTI-PATTERN: Fat repository interface forces all consumers to depend on all operations
public interface CrudRepository<T, ID> {
T save(T entity);
Optional<T> findById(ID id);
List<T> findAll();
void deleteById(ID id);
boolean existsById(ID id);
long count();
void saveAll(List<T> entities);
void deleteAll();
}java
// CORRECT: Segregated repository interfaces
public interface ReadableRepository<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
boolean existsById(ID id);
long count();
}
public interface WritableRepository<T, ID> {
T save(T entity);
void saveAll(List<T> entities);
}
public interface DeletableRepository<T, ID> {
void deleteById(ID id);
void deleteAll();
}
// Full CRUD: compose all three roles
public interface FullRepository<T, ID>
extends ReadableRepository<T, ID>,
WritableRepository<T, ID>,
DeletableRepository<T, ID> {
}
// Concrete JPA implementation
@Repository
public class JpaProductRepository implements FullRepository<Product, Long> {
@PersistenceContext
private EntityManager em;
@Override public Optional<Product> findById(Long id) {
return Optional.ofNullable(em.find(Product.class, id));
}
@Override public List<Product> findAll() {
return em.createQuery("SELECT p FROM Product p", Product.class).getResultList();
}
@Override public boolean existsById(Long id) { return findById(id).isPresent(); }
@Override public long count() {
return em.createQuery("SELECT COUNT(p) FROM Product p", Long.class).getSingleResult();
}
@Override @Transactional public Product save(Product p) { em.persist(p); return p; }
@Override @Transactional public void saveAll(List<Product> list) { list.forEach(em::persist); }
@Override @Transactional public void deleteById(Long id) {
findById(id).ifPresent(em::remove);
}
@Override @Transactional public void deleteAll() {
em.createQuery("DELETE FROM Product").executeUpdate();
}
}
// Reporting service only depends on reads — cannot accidentally call deleteAll()
@Service
public class ProductReportService {
private final ReadableRepository<Product, Long> productRepo;
// Constructor injection with the narrow interface
public ProductReportService(ReadableRepository<Product, Long> productRepo) {
this.productRepo = productRepo;
}
public List<Product> getProductCatalog() {
return productRepo.findAll();
}
public long getProductCount() {
return productRepo.count();
}
// productRepo.deleteAll() is not even accessible here — compile-time safety
}Diagrams
Segregated Interfaces Overview
Client Dependency Diagram — Before and After
Role-Based Interface Design
Repository Segregation Class Diagram
ISP Violation Detection
Best Practices
- Interface per role — Name interfaces after the capability they represent, not the class that implements them.
Workable,Reportable,Auditableare role names.IWorkerorWorkerInterfaceare anti-patterns. - Prefer many small interfaces over few large ones — Implementing five small interfaces is almost always better than implementing one large interface with unused methods. Java's multiple-interface inheritance makes this cost-free.
- Let clients define interfaces — The best interfaces emerge from what clients actually need, not from what implementors happen to provide. Work backwards from callers to define the minimal interface.
- Avoid marker interfaces for behavior — A marker interface (one with no methods) used solely to tag a class as having certain behavior is a code smell. Use annotations for tagging and proper role interfaces for behavior.
- Watch for
UnsupportedOperationException— Any occurrence of this exception in an interface implementation is a nearly certain ISP violation. It signals a class being forced to pretend it supports something it does not. - Compose at the class level — Like
HumanWorkerimplementingWorkable, Eatable, Sleepable, andFullRepositoryextending three smaller interfaces, composition at the type level keeps each interface honest while allowing rich implementations. - Apply ISP to generic parameters too — A method that accepts
Map<String, Object>when it only reads keys accepts more than it needs. Prefer narrower types or custom interfaces that express only the required capabilities.
Related Concepts
Other SOLID Principles:
- SRP — Single Responsibility Principle: ISP is SRP applied to interfaces. A fat interface has too many responsibilities just as a God Class does.
- OCP — Open/Closed Principle: Narrow interfaces are easier to extend without modification — a new implementation of
Reportabledoes not require changing the interface. - LSP — Liskov Substitution Principle: ISP makes LSP easier to satisfy. Narrow contracts are easier to honor completely, avoiding the partial-implementation problem.
- DIP — Dependency Inversion Principle: When injecting dependencies, inject the narrowest interface that satisfies the need.
ProductReportServicereceivesReadableRepository, notJpaProductRepository.
Further Reading:
- Role interfaces vs header interfaces (Martin Fowler).
- Java's
Comparable<T>,Iterable<T>,AutoCloseableas well-designed role interfaces. java.util.functionpackage:Supplier,Consumer,Predicate— standard library ISP in action.- REST API Design: applying ISP to API surface design and consumer-driven contracts.