Design Pattern

Interface Segregation Principle (ISP)

Clean Java-only production-ready implementation.


Core Idea

Clients should not depend on methods they do not use.

The Violation

A monolithic UserRepository interface forces every consumer to depend on methods they don't need.

When to Split

SignalAction
Interface has "and" in its nameUserReadAndWriteAndAudit โ†’ split
A method is only used by 20% of consumersThat method belongs in a separate interface
Changes to one method require recompiling many consumersThat consumer needs a narrower contract
throw new UnsupportedOperationException in any implementationThe interface is too broad for that implementor

Interview Checklist

  1. Start from USE CASES (what each client needs), not ENTITIES
  2. Design narrow interfaces around workflows
  3. Inject the smallest required contract in each service
  4. Combine interfaces only in concrete implementations (they implement multiple small interfaces)

Where This Appears in LLD Problems

  • Splitwise (#5): ExpenseReader, ExpenseWriter, BalanceCalculator (segregate from a monolithic ExpenseManager)
  • ATM (#13): CashDispenser, CardReader, AccountReader, TransactionWriter
  • Library Management (#12): BookCatalogReader, BorrowingManager, NotificationDispatcher
  • Cab Sharing (#23): TripReader, TripMatcher, PaymentCollector

The Fix

Split into narrow, role-based interfaces.

// โ”€โ”€โ”€ EXAMPLE 1 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// WHAT WE ARE IMPLEMENTING:
// A smart multifunction printer system where specialized machines implement
// only what they actually do.
//
// WHERE THE PRINCIPLE FITS IN:
// Split the monolithic Machine interface into highly cohesive interfaces:
// Printer, Scanner, and Fax. A basic printer implements only Printer,
// preventing unused methods.
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import java.util.List;

// --- Bad: Fat interface ---
interface UserRepository {
    User findById(String id);
    void save(User user);
    void delete(String id);
    void update(User user);
    List<User> findActiveUsers();
    List<UserAudit> getAuditTrail(String userId);
    AnalyticsReport getAnalytics(String query);
    void exportToCSV();
}

// Every consumer depends on all methods, even if they only need one.

// --- Good: Segregated interfaces ---
interface UserReader {
    User findById(String id);
    List<User> findActiveUsers();
}

interface UserWriter {
    void save(User user);
    void update(User user);
    void delete(String id);
}

interface UserAuditReader {
    List<UserAudit> getAuditTrail(String userId);
}

interface UserAnalyticsReader {
    AnalyticsReport getAnalytics(String query);
}

// --- Concrete implementation ---
class PostgresUserRepository implements UserReader, UserWriter, UserAuditReader, UserAnalyticsReader {
    public User findById(String id) {
        System.out.println("  [DB] SELECT * FROM users WHERE id = " + id);
        return new User(id, "alice");
    }
    public void save(User user) { System.out.println("  [DB] INSERT INTO users ..."); }
    public void update(User user) { System.out.println("  [DB] UPDATE users ..."); }
    public void delete(String id) { System.out.println("  [DB] DELETE FROM users ..."); }
    public List<User> findActiveUsers() { return List.of(new User("1", "alice"), new User("2", "bob")); }
    public List<UserAudit> getAuditTrail(String userId) {
        return List.of(new UserAudit(userId, "LOGIN", 1000L));
    }
    public AnalyticsReport getAnalytics(String query) { return new AnalyticsReport("results"); }
}

// --- Clients depend only on what they need ---
class UserAuthService {
    private final UserReader reader;
    public UserAuthService(UserReader reader) { this.reader = reader; }
    public User authenticate(String id) { return reader.findById(id); }
}

class UserRegistrationService {
    private final UserWriter writer;
    public UserRegistrationService(UserWriter writer) { this.writer = writer; }
    public void register(User user) { writer.save(user); }
}

class UserAdminService {
    private final UserAuditReader auditReader;
    public UserAdminService(UserAuditReader auditReader) { this.auditReader = auditReader; }
    public void showAudit(String userId) {
        auditReader.getAuditTrail(userId).forEach(a -> System.out.println("  Audit: " + a));
    }
}

// Utility classes for demo
record User(String id, String name) {}
record UserAudit(String userId, String action, long timestamp) {}
record AnalyticsReport(String data) {}

public class Main {
    public static void main(String[] args) {
        PostgresUserRepository repo = new PostgresUserRepository();

        UserAuthService auth = new UserAuthService(repo);
        auth.authenticate("42");

        UserRegistrationService reg = new UserRegistrationService(repo);
        reg.register(new User("99", "bob"));

        UserAdminService admin = new UserAdminService(repo);
        admin.showAudit("42");
    }
}

๐Ÿ’ฌReview

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...