Interface Segregation Principle


Introduction#

The Interface Segregation Principle states that clients should not be forced to depend on interfaces they don’t use. Put another way, many client specific interfaces are better than one general purpose interface. This principle protects your code from the ripple effects of change and prevents classes from carrying unnecessary baggage.

Understanding Interface Segregation#

Think about your codebase right now. You probably have at least one interface that feels too big, requiring implementers to write empty methods or throw “not implemented” exceptions. Every time you see a method that returns null or does nothing because a class doesn’t need that particular behavior, you’re witnessing a violation of the Interface Segregation Principle.

The principle emerged from Bob Martin’s consulting work at Xerox, where a single monolithic class handled every type of print job the system could process. Each job type had to know about and account for every other job type, creating a tangled web of dependencies. Martin solved this by breaking the functionality into focused, purposeful interfaces that different clients could use independently.

The Problem with Fat Interfaces#

When interfaces grow too large, they create several problems. First, they force coupling between unrelated parts of your system. If you have a User interface with methods for authentication, profile management, billing, and notifications, any change to one aspect affects every class implementing that interface. This coupling makes your system brittle and resistant to change.

Second, fat interfaces lead to confusing implementations. When a class implements an interface but only uses half its methods, developers reading that code waste time understanding why certain methods exist or wondering if they’re missing something important. This cognitive overhead accumulates across your team, slowing development and increasing the chance of bugs.

Consider this common violation in a document processing system:

interface Document {
  open(): void;
  save(): void;
  print(): void;
  fax(): void;
  scan(): void;
  email(): void;
  compress(): void;
  encrypt(): void;
}

class ReadOnlyPDF implements Document {
  open(): void {
    // can open the PDF
  }
  
  save(): void {
    throw new Error("Cannot save read-only document");
  }
  
  print(): void {
    // can print the PDF
  }
  
  fax(): void {
    throw new Error("Faxing not supported");
  }
  
  scan(): void {
    throw new Error("Already a digital document");
  }
  
  email(): void {
    // can email the PDF
  }
  
  compress(): void {
    throw new Error("Read-only document cannot be modified");
  }
  
  encrypt(): void {
    throw new Error("Read-only document cannot be modified");
  }
}

The ReadOnlyPDF class is forced to implement eight methods when it can meaningfully support only three. This creates a dishonest interface where the type system promises capabilities that the implementation cannot deliver.

Better Interfaces#

The solution is to break large interfaces into smaller, focused ones:

interface Readable {
  open(): void;
}

interface Printable {
  print(): void;
}

interface Saveable {
  save(): void;
}

interface Emailable {
  email(): void;
}

interface Faxable {
  fax(): void;
}

interface Compressible {
  compress(): void;
}

interface Encryptable {
  encrypt(): void;
}

class ReadOnlyPDF implements Readable, Printable, Emailable {
  open(): void {
    // can open the PDF
  }
  
  print(): void {
    // can print the PDF
  }
  
  email(): void {
    // can email the PDF
  }
}

class EditableDocument implements Readable, Printable, Saveable, 
                                  Emailable, Compressible, Encryptable {
  // implements all the interfaces it actually supports
}

Now each class implements only the interfaces relevant to its behavior. The type system accurately reflects what each implementation can actually do.

Real-World Examples#

Payment Systems#

In payment systems, fat interfaces create problems:

// Before: Fat interface
interface PaymentMethod {
  charge(amount: number): void;
  refund(transactionId: string): void;
  void(transactionId: string): void;
  partialRefund(transactionId: string, amount: number): void;
  recurringCharge(amount: number, frequency: string): void;
  updateCard(cardDetails: CardInfo): void;
  getTransactionHistory(): Transaction[];
  dispute(transactionId: string): void;
}

Better design segregates these concerns:

interface Chargeable {
  charge(amount: number): void;
}

interface Refundable {
  refund(transactionId: string): void;
}

interface PartiallyRefundable {
  partialRefund(transactionId: string, amount: number): void;
}

interface Recurring {
  recurringCharge(amount: number, frequency: string): void;
}

interface Disputable {
  dispute(transactionId: string): void;
}

class CreditCard implements Chargeable, Refundable, 
                            PartiallyRefundable, Recurring, Disputable {
  // Credit cards support all these operations
}

class GiftCard implements Chargeable, Refundable {
  // Gift cards only support basic charge and full refund
}

class BankTransfer implements Chargeable, Refundable, PartiallyRefundable {
  // Bank transfers support charges and refunds but not recurring
}

API Design#

The same principle applies to API design:

// Before: One bloated endpoint
GET /api/user/{id}
Returns: {
  profile: {...},
  preferences: {...},
  billing: {...},
  activity: {...},
  permissions: {...},
  audit: {...}
}

// After: Segregated endpoints
GET /api/user/{id}/profile
GET /api/user/{id}/preferences  
GET /api/user/{id}/billing
GET /api/user/{id}/activity
GET /api/user/{id}/permissions
GET /api/user/{id}/audit

Clients only fetch what they need, reducing bandwidth and coupling.

Relationship to Other SOLID Principles#

The Interface Segregation Principle works in concert with the other SOLID principles:

  • Single Responsibility Principle ensures classes have focused responsibilities that are more likely to remain stable
  • Liskov Substitution Principle guarantees that implementations can replace interfaces without breaking existing functionality
  • Open/Closed Principle becomes easier when interfaces are specific and focused
  • Dependency Inversion Principle depends on well-defined abstractions, which segregated interfaces provide

Practical Implementation#

Implementing ISP requires identifying variation points in your system and creating appropriate abstractions. Key practices include:

  1. Break interfaces into client-specific contracts rather than omnibus interfaces
  2. Implement only what’s needed - if a class doesn’t implement a method, that method shouldn’t be in its interface
  3. Use composition to combine multiple focused interfaces when needed
  4. Consider the clients - segregate interfaces based on how they’re actually used
  5. Refactor when you see “not implemented” - that’s a signal that your interface is too fat

Conclusion#

The Interface Segregation Principle recognizes that large, general-purpose interfaces create fragile systems. By creating focused, client-specific interfaces, you reduce coupling, improve clarity, and make your code easier to change.

When you find yourself writing stub implementations or throwing “not implemented” exceptions, that’s a clear signal to segregate your interfaces. Your codebase will become more flexible and your type system will more accurately reflect what your code can actually do.