Interface Segregation Principle
Core Idea
Interface Segregation Principle states that a client should depend only on methods it needs. A wide interface forces clients to import behavior outside their own responsibility. That pressure creates accidental coupling and brittle change paths.
A narrow interface maps to one role. A role interface gives one contract with clear boundaries. Each client depends on a small contract that reflects its job. This model keeps integration cost predictable.
Robert C. Martin framed this principle as part of SOLID. The practical point is direct. Do not force unused method dependencies into client code.
Conceptual Overview
The shared system context in this handbook is the car intelligence and marketplace platform. Examples in this page follow that context: Hypothetical Scenario.
The platform runs many workflows. The recommendation engine reads market and reliability data. The marketplace publishing workflow writes listing updates. The ownership-cost analytics workflow runs batch reports. A fraud review workflow flags suspicious seller behavior.
One fat service contract can expose all those operations in one place. That design looks simple in early phases. The cost appears after feature growth. A recommendation client imports write methods it never calls. A write path client imports analytics operations it never calls. Each client now depends on methods and exceptions outside its use case.
Any change in that fat contract can trigger edit waves. One new method may require mock updates in unrelated tests. One changed method signature can force client recompilation in broad areas. This drift breaks modularity.
Interface segregation solves that drift through role contracts. Each client receives one focused interface. The recommendation engine gets a read contract. The publisher gets a write contract. Analytics gets a reporting contract. A concrete adapter may implement many contracts, yet client code sees only one role view.
This first diagram shows dependency spread under a fat interface. It then contrasts the same clients with focused role contracts.
This second diagram shows one adapter behind several client specific interfaces. Each client depends on one narrow surface.
Computing History
David Parnas wrote in 1972 that modules should hide design decisions likely to change. This work shaped later guidance on separation of responsibilities and interface boundaries. Robert C. Martin formalized Interface Segregation Principle in SOLID literature during the 1990s.
Sources: Parnas (1972) and Martin (2003)
Quote
"Clients should not be forced to depend upon interfaces that they do not use."
Source: Robert C. Martin, 2003
Generic Example: Python
Non compliant contract:
from dataclasses import dataclass
@dataclass(frozen=True)
class ListingQuery:
city: str
max_budget: int
@dataclass(frozen=True)
class ListingDraft:
title: str
city: str
price: int
class CarDataService:
def find_listings(self, query: ListingQuery) -> list[dict]:
raise NotImplementedError
def create_listing(self, draft: ListingDraft) -> str:
raise NotImplementedError
def generate_ownership_report(self, seller_id: str) -> dict:
raise NotImplementedError
class MatchCandidatesUseCase:
def __init__(self, service: CarDataService):
self.service = service
def execute(self, query: ListingQuery) -> list[dict]:
return self.service.find_listings(query)
MatchCandidatesUseCase only needs find_listings.
The class still depends on write and report methods.
This coupling is unnecessary.
Compliant segregated contracts with Protocol:
from typing import Protocol
class ListingReader(Protocol):
def find_listings(self, query: ListingQuery) -> list[dict]:
...
class ListingWriter(Protocol):
def create_listing(self, draft: ListingDraft) -> str:
...
class OwnershipReporter(Protocol):
def generate_ownership_report(self, seller_id: str) -> dict:
...
class MatchCandidatesUseCase:
def __init__(self, reader: ListingReader):
self.reader = reader
def execute(self, query: ListingQuery) -> list[dict]:
return self.reader.find_listings(query)
Now each client binds to one role interface.
mypy checks shape conformance for each protocol.
A single adapter can implement many role contracts:
class MongoMarketplaceAdapter:
def find_listings(self, query: ListingQuery) -> list[dict]:
return []
def create_listing(self, draft: ListingDraft) -> str:
return "listing-1001"
def generate_ownership_report(self, seller_id: str) -> dict:
return {"seller_id": seller_id, "avg_yearly_cost": 4200}
Client code can receive the same adapter instance through different protocol types. Each client still sees one narrow interface.
Generic Example: TypeScript
type ListingQuery = {
city: string;
maxBudget: number;
};
type ListingDraft = {
title: string;
city: string;
price: number;
};
interface ListingReader {
findListings(query: ListingQuery): Promise<Array<{ id: string; price: number }>>;
}
interface ListingWriter {
createListing(draft: ListingDraft): Promise<string>;
}
interface OwnershipReporter {
generateOwnershipReport(sellerId: string): Promise<{ sellerId: string; avgYearlyCost: number }>;
}
class MarketplaceAdapter implements ListingReader, ListingWriter, OwnershipReporter {
async findListings(query: ListingQuery) {
return [{ id: "car-410", price: 21000 }].filter((x) => x.price <= query.maxBudget);
}
async createListing(_draft: ListingDraft) {
return "listing-410";
}
async generateOwnershipReport(sellerId: string) {
return { sellerId, avgYearlyCost: 4300 };
}
}
class MatchCandidatesUseCase {
constructor(private readonly reader: ListingReader) {}
async execute(query: ListingQuery) {
return this.reader.findListings(query);
}
}
MatchCandidatesUseCase cannot call write or report methods.
The type system guards the client boundary.
Generic Example: Rust
struct ListingQuery {
city: String,
max_budget: i32,
}
struct ListingDraft {
title: String,
city: String,
price: i32,
}
trait ListingReader {
fn find_listings(&self, query: &ListingQuery) -> Vec<(String, i32)>;
}
trait ListingWriter {
fn create_listing(&self, draft: &ListingDraft) -> String;
}
trait OwnershipReporter {
fn generate_ownership_report(&self, seller_id: &str) -> i32;
}
struct MarketplaceAdapter;
impl ListingReader for MarketplaceAdapter {
fn find_listings(&self, query: &ListingQuery) -> Vec<(String, i32)> {
vec![("car-510".to_string(), 22000)]
.into_iter()
.filter(|(_, price)| *price <= query.max_budget)
.collect()
}
}
impl ListingWriter for MarketplaceAdapter {
fn create_listing(&self, _draft: &ListingDraft) -> String {
"listing-510".to_string()
}
}
impl OwnershipReporter for MarketplaceAdapter {
fn generate_ownership_report(&self, _seller_id: &str) -> i32 {
4400
}
}
struct MatchCandidatesUseCase<R: ListingReader> {
reader: R,
}
impl<R: ListingReader> MatchCandidatesUseCase<R> {
fn execute(&self, query: &ListingQuery) -> Vec<(String, i32)> {
self.reader.find_listings(query)
}
}
Trait bounds keep the client contract narrow.
The use case depends only on ListingReader.
Link with Other Principles
Link with Dependency Inversion: Inversion points dependencies to abstractions. Segregation defines the size and shape of those abstractions.
Link with Liskov Substitution Principle: A smaller interface gives fewer behavioral rules to preserve. That reduction improves substitution reliability.
Link with Open-Closed Principle: New roles can enter through new interfaces or new adapters. Stable clients stay closed to unrelated method growth.
Link with Demeter's Law: Narrow role interfaces reduce deep traversal pressure. Clients call one collaborator through one role contract.
Trade-offs and Failure Modes
Interface segregation introduces more interfaces. A team can create too many tiny contracts with weak names. That shape can hurt discoverability. A second risk appears in naming drift. Two interfaces may overlap without clear ownership.
Common failure modes:
- one broad interface grows through convenience additions
- role interfaces split without domain meaning
- adapter classes expose role leakage through shared mutable state
- tests mock role contracts with weak behavioral assertions
Control tactics:
- define interfaces from client use cases, not from data tables
- keep one role name tied to one capability boundary
- review interface additions in architecture review
- pair interface contracts with contract tests
Foundational Principle Links
This principle links to Abstraction and Boundaries. Role interfaces define explicit capability boundaries.
This principle links to Modularity and Composition. Focused interfaces support cleaner module composition.
This principle links to Simplicity First. Smaller contracts reduce cognitive load during design and code review.
This principle links to Correctness and Testing. Narrow contracts make contract test scope precise.
Practice Checklist
- map each client to the exact methods it needs
- split broad interfaces into role interfaces
- keep role names tied to domain language
- pass narrow interfaces into constructors
- run static checks such as
mypyor TypeScript compiler checks - run contract tests for each role interface
- reject interface growth that crosses capability boundaries
Written by: Pedro Guzmán
See References for complete APA-style bibliographic entries used on this page.