1. Introduction
The Interface Segregation Principle (ISP) is one of the SOLID design principles used to create focused, maintainable, and safe abstractions.
In practical Java development, ISP means:
A class should not be forced to depend on methods that it does not need or cannot meaningfully implement.
Large interfaces often look convenient at first because developers can place all related operations in one contract. In real enterprise applications, however, different implementations frequently support different capabilities.
For example, a document-management system may contain:
- Local file storage
- Amazon S3 storage
- Read-only archive storage
- Temporary storage
- Public CDN storage
If one large StorageService interface contains every possible operation, some implementations may be forced to provide meaningless methods or throw exceptions.
That usually creates:
UnsupportedOperationException- Empty method implementations
- Fake return values
- Unnecessary dependencies
- Large mocks in unit tests
- Difficult maintenance
- Tight coupling between unrelated features
ISP solves this by encouraging developers to create small, cohesive interfaces based on actual client needs and business capabilities.
2. What This Topic Means
Consider the following interface:
public interface EmployeeService {
Employee create(Employee employee);
Employee update(Employee employee);
void delete(Long employeeId);
Employee findById(Long employeeId);
List<Employee> findAll();
byte[] generateSalaryReport(Long employeeId);
void approveLeave(Long employeeId);
void resetPassword(Long employeeId);
}This interface contains many operations related to employees.
However, different consumers may need only specific functionality.
For example:
EmployeeController may need:
create()
update()
findById()EmployeeReportService may need:
generateSalaryReport()LeaveManagementService may need:
approveLeave()PasswordAdministrationService may need:
resetPassword()If every client depends on the complete EmployeeService interface, they are coupled to methods they never use.
ISP recommends exposing narrower contracts such as:
EmployeeReader
EmployeeWriter
EmployeeReportGenerator
LeaveApprover
EmployeePasswordManagerThe purpose is not to create tiny interfaces mechanically.
The goal is to create interfaces whose methods represent a cohesive responsibility or capability.
3. Why It Matters in Real Projects
Readability
A focused interface immediately communicates what the consumer needs.
Compare:
private final EmployeeService employeeService;with:
private final EmployeeReader employeeReader;The second dependency gives much clearer architectural information.
Maintainability
Changes to unrelated functionality should not affect clients that do not use that functionality.
If salary-report generation changes, employee lookup consumers should ideally remain unaffected.
Testability
Large interfaces create unnecessarily large mocks.
A test may need:
mock(EmployeeService.class)even though the tested class uses only findById().
With a focused interface:
mock(EmployeeReader.class)the test dependency is clearer.
Reliability
Implementations are less likely to contain unsupported, empty, or fake method implementations.
Team Development
Different teams can work on separate capabilities without continuously changing one shared interface.
For example:
- Identity team
- Payroll team
- Leave-management team
- Employee-profile team
can work through separate contracts.
Scalability of Design
As an application grows, capability-specific interfaces make it easier to add implementations without forcing unrelated methods onto them.
4. Core Concept
ISP is usually stated as:
Clients should not be forced to depend on interfaces they do not use.
The word client is important.
ISP is not merely about interface size.
An interface containing ten methods is not automatically bad.
An interface containing three unrelated methods may already violate ISP.
The correct question during code review is:
Do these methods belong together from the perspective of the clients using this abstraction?
Example
Consider:
public interface NotificationService {
void sendEmail(Notification notification);
void sendSms(Notification notification);
void sendPush(Notification notification);
}Now suppose there are separate implementations:
public class EmailNotificationService implements NotificationService {
@Override
public void sendEmail(Notification notification) {
// Send email
}
@Override
public void sendSms(Notification notification) {
throw new UnsupportedOperationException();
}
@Override
public void sendPush(Notification notification) {
throw new UnsupportedOperationException();
}
}The problem is not merely that the interface contains three methods.
The problem is that an email implementation is forced to implement SMS and push-notification behavior that it does not support.
A better contract is:
public interface NotificationSender {
void send(Notification notification);
}Then:
public class EmailNotificationSender implements NotificationSender {
@Override
public void send(Notification notification) {
// Send email
}
}and:
public class SmsNotificationSender implements NotificationSender {
@Override
public void send(Notification notification) {
// Send SMS
}
}Now every implementation genuinely supports its contract.
5. Important Rules
When designing or reviewing Java interfaces:
- Keep interfaces focused around one cohesive capability.
- Do not add methods merely because they belong to the same broad domain.
- Avoid forcing implementations to throw
UnsupportedOperationException. - Avoid empty implementations required only to satisfy an interface.
- Avoid methods that return fake values because the implementation cannot support them.
- Design interfaces from the consumer's needs, not only from the implementation's perspective.
- Prefer capability-based interfaces where implementations support different features.
- Avoid large interfaces that become dumping grounds for future methods.
- Do not split interfaces purely to maximize the number of interfaces.
- Keep related operations together when clients genuinely use them together.
- Review Spring services for dependencies that expose far more functionality than required.
- Keep repository abstractions focused when different clients need different data access capabilities.
- Use composition when multiple focused interfaces are needed by one implementation.
- Consider ISP together with SRP, LSP, and Dependency Inversion.
- Make unsupported states difficult to express through the type system.
6. Bad Code Example
Consider an e-commerce application with a large order-management interface.
public interface OrderOperations {
Order createOrder(CreateOrderRequest request);
Order getOrder(Long orderId);
Order cancelOrder(Long orderId);
RefundResult refundOrder(Long orderId);
ShipmentResult shipOrder(Long orderId);
Invoice generateInvoice(Long orderId);
}An order-query service only needs to retrieve orders:
@Service
public class CustomerOrderQueryService {
private final OrderOperations orderOperations;
public CustomerOrderQueryService(OrderOperations orderOperations) {
this.orderOperations = orderOperations;
}
public Order getCustomerOrder(Long orderId) {
return orderOperations.getOrder(orderId);
}
}A third-party order implementation supports order creation and lookup but does not support refunding or shipping.
@Component
public class ExternalMarketplaceOrderOperations implements OrderOperations {
@Override
public Order createOrder(CreateOrderRequest request) {
return createMarketplaceOrder(request);
}
@Override
public Order getOrder(Long orderId) {
return fetchMarketplaceOrder(orderId);
}
@Override
public Order cancelOrder(Long orderId) {
throw new UnsupportedOperationException("Cancellation is managed by marketplace");
}
@Override
public RefundResult refundOrder(Long orderId) {
throw new UnsupportedOperationException("Refund is managed by marketplace");
}
@Override
public ShipmentResult shipOrder(Long orderId) {
throw new UnsupportedOperationException("Shipping is managed by marketplace");
}
@Override
public Invoice generateInvoice(Long orderId) {
return null;
}
private Order createMarketplaceOrder(CreateOrderRequest request) {
return new Order();
}
private Order fetchMarketplaceOrder(Long orderId) {
return new Order();
}
}The class technically implements OrderOperations, but most of the contract is meaningless for this implementation.
7. Problems in the Bad Code
Large Multi-Capability Interface
OrderOperations mixes several responsibilities:
- Order creation
- Order lookup
- Cancellation
- Refund
- Shipping
- Invoice generation
These capabilities do not necessarily belong to every order implementation.
Unsupported Methods
The marketplace implementation throws:
UnsupportedOperationExceptionfor several interface methods.
This indicates that the interface demands capabilities the implementation does not possess.
Fake Implementation
generateInvoice() returns:
nullThis may cause downstream NullPointerException or incorrect business behavior.
Unnecessary Client Dependency
CustomerOrderQueryService uses only:
getOrder()but depends on an abstraction exposing destructive operations such as:
cancelOrder()
refundOrder()That dependency is wider than necessary.
Maintenance Problem
Adding another operation to OrderOperations may require updating every implementation, even implementations that have no need for the operation.
Testing Problem
Tests for CustomerOrderQueryService must mock a broad interface even though only one operation matters.
Production Risk
Unsupported operations may remain hidden until a specific implementation is selected at runtime.
8. Code Review Findings
A senior Java developer should notice the following during PR review.
Finding 1
OrderOperations contains multiple independent capabilities that not every implementation can support.
Finding 2
The presence of several UnsupportedOperationException implementations is a strong signal that the abstraction is too broad.
Finding 3
Returning null from generateInvoice() hides an unsupported capability rather than representing it accurately.
Finding 4
CustomerOrderQueryService depends on much more functionality than it actually requires.
Finding 5
Future interface changes will create unnecessary implementation churn.
Finding 6
The design makes it possible to call methods that appear valid at compile time but fail at runtime.
Finding 7
Order lookup, refund, shipping, and invoice generation should be reviewed as separate capabilities rather than automatically placed into one large service interface.
9. Reviewer Comment Example
A useful PR review comment could be:
OrderOperationsrequires marketplace implementations to provide refund, shipping, cancellation, and invoice methods they do not support. Consider splitting these into focused capability interfaces so each implementation depends only on the operations it actually provides.
Another comment:
CustomerOrderQueryServiceonly requires order lookup. Injecting the completeOrderOperationscontract creates unnecessary coupling. Could this consumer depend on anOrderReaderinterface instead?
Another:
Returning
nullfromgenerateInvoice()hides the fact that this implementation does not support invoice generation. It would be safer to model invoice generation as a separate capability.
10. Improved Code
Separate the major capabilities.
public interface OrderCreator {
Order createOrder(CreateOrderRequest request);
}
public interface OrderReader {
Order getOrder(Long orderId);
}
public interface OrderCanceller {
Order cancelOrder(Long orderId);
}
public interface OrderRefundProcessor {
RefundResult refundOrder(Long orderId);
}
public interface OrderShipmentProcessor {
ShipmentResult shipOrder(Long orderId);
}
public interface OrderInvoiceGenerator {
Invoice generateInvoice(Long orderId);
}The external marketplace implementation now implements only supported capabilities.
@Component
public class ExternalMarketplaceOrderService implements OrderCreator, OrderReader {
private final MarketplaceClient marketplaceClient;
public ExternalMarketplaceOrderService(MarketplaceClient marketplaceClient) {
this.marketplaceClient = marketplaceClient;
}
@Override
public Order createOrder(CreateOrderRequest request) {
return marketplaceClient.createOrder(request);
}
@Override
public Order getOrder(Long orderId) {
return marketplaceClient.getOrder(orderId);
}
}The query service depends only on the contract it requires.
@Service
public class CustomerOrderQueryService {
private final OrderReader orderReader;
public CustomerOrderQueryService(OrderReader orderReader) {
this.orderReader = orderReader;
}
public Order getCustomerOrder(Long orderId) {
return orderReader.getOrder(orderId);
}
}An internal implementation may support more capabilities.
@Service
public class InternalOrderService implements OrderCreator, OrderReader, OrderCanceller, OrderRefundProcessor {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
public InternalOrderService(OrderRepository orderRepository, PaymentService paymentService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
}
@Override
public Order createOrder(CreateOrderRequest request) {
Order order = Order.from(request);
return orderRepository.save(order);
}
@Override
public Order getOrder(Long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
@Override
public Order cancelOrder(Long orderId) {
Order order = getOrder(orderId);
order.cancel();
return orderRepository.save(order);
}
@Override
public RefundResult refundOrder(Long orderId) {
Order order = getOrder(orderId);
return paymentService.refund(order.getPaymentTransactionId());
}
}11. Improved Code Explanation
Interfaces Represent Actual Capabilities
Each interface now expresses one cohesive operation area.
For example:
OrderReadermeans that an implementation can retrieve orders.
Clients Depend on What They Use
CustomerOrderQueryService depends only on:
OrderReaderIt no longer knows about:
- Cancellation
- Refunds
- Shipping
- Invoice generation
Unsupported Methods Disappear
ExternalMarketplaceOrderService does not implement capabilities it cannot provide.
There is no need for:
UnsupportedOperationExceptionor fake null responses.
Implementations Can Combine Interfaces
ISP does not mean every implementation must support only one interface.
InternalOrderService can implement several interfaces because it genuinely supports several capabilities.
Future Changes Become Safer
If invoice-generation requirements change, implementations that do not generate invoices are unaffected.
12. Bad Code vs Improved Code
| Area | Bad Code | Improved Code |
|---|---|---|
| Interface design | One broad interface | Focused capability interfaces |
| Client dependency | Depends on unnecessary methods | Depends only on required methods |
| Unsupported behavior | Runtime exceptions | Unsupported methods are absent |
| Maintainability | Interface changes affect many classes | Changes remain localized |
| Testability | Large mocks | Small focused mocks |
| Readability | Consumer capability is unclear | Dependency communicates intent |
| Reliability | Valid-looking calls may fail | Type system limits invalid usage |
| Extensibility | New providers may require dummy methods | Providers implement supported capabilities |
13. Real Project Scenario
Consider a healthcare platform processing patient documents.
Different storage providers are available:
- Internal secure document storage
- AWS S3
- Read-only historical archive
- Temporary upload storage
- External laboratory document storage
A developer creates:
public interface MedicalDocumentStorage {
void upload(Document document);
Document download(String documentId);
void delete(String documentId);
String createPublicUrl(String documentId);
void archive(String documentId);
void restore(String documentId);
}The internal storage system supports:
- Upload
- Download
- Delete
- Archive
- Restore
The historical archive supports:
- Download
- Restore
External laboratory storage supports:
- Download
None of the medical systems should expose public URLs due to security requirements.
If every provider implements the complete interface, developers may introduce:
UnsupportedOperationExceptionor dangerous placeholder implementations.
A better design would use capabilities such as:
DocumentReader
DocumentWriter
DocumentDeleter
DocumentArchiver
DocumentRestorerConsumers then request exactly the capability needed for each workflow.
For example, a patient-view service may depend only on:
DocumentReaderwhile retention-management jobs depend on:
DocumentArchiverThis produces clearer security boundaries and a more accurate domain model.
14. Production Impact
Poor interface segregation can cause several production problems.
Runtime Exceptions
A method exposed by an interface may fail only for certain implementations.
Example:
storage.delete(documentId);may work with S3 but fail with read-only archive storage.
Hidden No-Op Behavior
An implementation may silently do nothing:
public void delete(String id) {
}The caller may assume deletion succeeded even though data remains.
Incorrect Business Results
Fake values such as:
return null;
return false;
return Collections.emptyList();may hide unsupported functionality.
Difficult Debugging
Errors become implementation-specific.
The same interface call may behave differently depending on the injected Spring bean.
Deployment Risk
A new provider may work correctly in basic tests but fail when a rarely used interface method is triggered in production.
Maintenance Cost
Changes to large interfaces require modifications across unrelated implementations and tests.
15. Common Developer Mistakes
Mistake 1: Creating a "God Interface"
Example:
interface UserService {
createUser();
updateUser();
deleteUser();
authenticateUser();
sendEmail();
generateReport();
exportCsv();
resetPassword();
assignRole();
}These responsibilities should not automatically be grouped together.
Mistake 2: Splitting Interfaces Only by Method Count
ISP does not mean:
Every interface should have one method.
Several related methods can belong together.
Mistake 3: Adding Methods to an Existing Interface for Convenience
A developer may think:
We already have
CustomerService; I will addexportCustomers()there.
That can force every implementation and client to absorb an unrelated concern.
Mistake 4: Throwing UnsupportedOperationException
This usually means the implementation does not truly support the complete contract.
Mistake 5: Empty Overrides
Example:
@Override
public void archive(Order order) {
}This silently violates caller expectations.
Mistake 6: Returning Dummy Data
Example:
@Override
public List<Order> getHistoricalOrders() {
return Collections.emptyList();
}An empty result should mean "no data", not "feature unsupported".
Mistake 7: Consumer Depends on Broad Service
A read-only controller may depend on a service exposing deletion and administration operations.
Mistake 8: Over-Segregation
Creating one interface for every single method can make the code harder to navigate without producing meaningful architectural separation.
16. Edge Cases
Implementations Supporting Multiple Capabilities
An implementation can implement several focused interfaces.
That does not violate ISP.
Example:
public class InternalOrderService implements OrderReader, OrderCreator, OrderCanceller {
}The important point is that the implementation genuinely supports all three contracts.
Shared Methods Used Together
Do not separate methods that clients consistently use as one cohesive capability.
For example:
beginTransaction()
commitTransaction()
rollbackTransaction()may reasonably belong to the same transaction contract.
Read-Only Implementations
If some implementations are read-only, avoid forcing them to implement write operations.
Optional External Provider Features
External APIs often differ by provider.
Reviewers should verify that interfaces represent only universally supported operations or intentionally separate provider capabilities.
Default Methods
Java interfaces support default methods.
Do not use default methods merely to hide interface-design problems.
Example:
default void delete(String id) {
throw new UnsupportedOperationException();
}This still leaves clients exposed to unsupported behavior.
Null Return Values
Do not use null as a substitute for "this implementation does not support this method."
Unsupported capability and absent data are different concepts.
17. Performance Considerations
ISP is primarily a maintainability and dependency-design principle.
It does not directly change algorithmic complexity.
Splitting:
LargeServiceinto:
Reader
Writer
Reporterdoes not automatically make an operation faster.
However, interface design can indirectly affect performance.
Unnecessary Expensive Dependencies
A broad service implementation may initialize:
- External API clients
- Reporting engines
- Database repositories
- Cache services
even when a particular client uses only one small capability.
Focused services can reduce accidental coupling to expensive infrastructure.
Accidental Remote Calls
A broad abstraction may hide whether a seemingly simple operation performs remote work.
Focused contracts can make such architectural boundaries clearer.
Testing Performance
Focused unit-test dependencies can reduce setup complexity, although this is usually not a meaningful production optimization.
Therefore:
Do not justify ISP primarily as a performance optimization. Its main value is cleaner contracts, lower coupling, maintainability, and reliability.
18. Security Considerations
ISP can contribute to stronger security boundaries when interfaces expose privileged operations.
Consider:
public interface UserManagementService {
User findUser(Long id);
User updateUser(User user);
void deleteUser(Long id);
void assignAdminRole(Long id);
}A read-only reporting component may need only:
findUser()but receives access to an abstraction exposing:
deleteUser()
assignAdminRole()Even if the component does not currently call these methods, exposing unnecessary privileged capabilities increases coupling and creates opportunities for misuse.
A better dependency is:
public interface UserReader {
User findUser(Long id);
}Privileged administration logic can depend on:
public interface UserAdministrator {
void deleteUser(Long id);
void assignAdminRole(Long id);
}ISP does not replace authorization controls.
Security-sensitive operations must still enforce:
- Authentication
- Authorization
- Tenant isolation
- Audit logging
- Input validation
However, narrower contracts reduce unnecessary exposure of privileged operations and improve architectural least-privilege design.
19. Testing Considerations
Unit Tests
Tests should verify each focused contract independently.
For example:
OrderReadershould test:
- Existing order retrieval
- Missing order behavior
- Invalid identifier handling
- Repository failure behavior
Consumer Tests
CustomerOrderQueryService should mock only:
OrderReaderThis makes tests easier to understand.
Implementation Tests
If InternalOrderService implements multiple interfaces, test each capability independently.
Negative Tests
Verify that invalid business states are handled correctly.
For example:
- Cancelling an already shipped order
- Refunding an unpaid order
- Retrieving a nonexistent order
Integration Tests
External provider implementations should be tested only for capabilities they claim to support.
Do not create meaningless tests for unsupported functionality.
Architecture Tests
Large projects can use architectural testing tools such as ArchUnit to verify dependency boundaries.
For example:
- Reporting package should depend only on read interfaces.
- Administration package may depend on write interfaces.
- Controllers should not depend directly on repository implementations.
20. Refactoring Guidelines
When an existing large interface is widely used, refactor incrementally.
Step 1: Identify Interface Clients
Search all references to the large interface.
Determine which methods each consumer actually uses.
Example:
CustomerController -> getOrder()
AdminOrderController -> cancelOrder(), getOrder()
RefundService -> refundOrder()
InvoiceService -> generateInvoice()Step 2: Group Related Capabilities
Create cohesive interfaces such as:
OrderReader
OrderCanceller
OrderRefundProcessor
OrderInvoiceGeneratorStep 3: Let Existing Implementation Implement Multiple Interfaces
You do not need to split the implementation immediately.
Example:
public class OrderService implements OrderReader, OrderCanceller, OrderRefundProcessor {
}Step 4: Change Consumers Gradually
Change:
private final OrderOperations orderOperations;to:
private final OrderReader orderReader;where appropriate.
Step 5: Update Tests
Replace broad mocks with focused mocks.
Step 6: Remove Unsupported Methods
After callers migrate, remove legacy methods that required dummy implementations.
Step 7: Preserve Business Behavior
Do not combine interface segregation with unrelated business-logic rewrites.
Keeping refactoring behavior-neutral reduces risk.
21. Best Practices
- Design interfaces around cohesive client needs.
- Make implementation capabilities explicit.
- Use multiple interfaces when a class genuinely supports several capabilities.
- Keep read and write concerns separate when consumers use them independently.
- Keep privileged administrative capabilities separate from ordinary read operations.
- Avoid forcing third-party adapters to implement unsupported features.
- Prefer meaningful interface names such as
OrderReaderover vague names such asCommonService. - Use constructor injection with the narrowest useful interface.
- Keep external integrations behind provider-appropriate abstractions.
- Refactor interfaces when repeated unsupported methods appear.
- Use contract tests for shared behavior.
- Review interface changes carefully because they can affect many implementations.
- Keep interface design aligned with real business capabilities.
- Avoid both giant interfaces and meaningless one-method fragmentation.
22. Practices to Avoid
Giant Service Interfaces
Avoid interfaces containing unrelated operations simply because they belong to the same domain entity.
Empty Method Implementations
Avoid:
@Override
public void export() {
}The caller cannot determine that nothing happened.
UnsupportedOperationException as Normal Design
Avoid:
@Override
public void refund() {
throw new UnsupportedOperationException();
}This usually means the interface is too broad.
Dummy Return Values
Avoid:
return null;
return false;
return 0;when the real meaning is "not supported."
Default Methods Hiding Unsupported Capabilities
Avoid:
default void archive() {
throw new UnsupportedOperationException();
}This moves the smell into the interface rather than solving it.
Client-Specific Branching
Avoid:
if (service instanceof ReadOnlyService) {
...
}The type system should represent capability differences where practical.
Mechanical Over-Segregation
Avoid creating dozens of tiny interfaces without a clear client or architectural reason.
23. Code Review Checklist
Ask these questions during Pull Request review:
- Does this interface contain methods used by completely different clients?
- Does every implementation genuinely support every method in the interface?
- Does any implementation throw
UnsupportedOperationException? - Does any implementation contain empty overridden methods?
- Does any method return a dummy value because the capability is unsupported?
- Does this consumer depend on methods it never uses?
- Could the consumer depend on a narrower interface?
- Are read and write capabilities mixed unnecessarily?
- Are privileged operations exposed to clients that only need read access?
- Are unrelated reporting, export, notification, or administration methods added to a core service interface?
- Will adding this interface method force unrelated implementations to change?
- Are third-party provider differences modeled explicitly?
- Would capability-specific interfaces better represent the domain?
- Are interfaces being split for a meaningful architectural reason rather than just reducing method count?
- Can one implementation implement multiple smaller interfaces cleanly?
- Are interface names clear about the capability they expose?
- Does the design eliminate unsupported runtime operations?
- Are unit tests mocking a broader dependency than required?
- Would a new implementation need dummy methods to satisfy this contract?
- Is the interface designed from client needs rather than convenience?
24. Common Pull Request Review Comments
- *This implementation supports only two of the six methods in the interface and throws UnsupportedOperationException for the others. Can we split the contract into supported capabilities?*
- *This controller only reads orders, but it currently depends on an interface that also exposes cancellation and refund operations. Could it depend on OrderReader instead?*
- *Adding exportCsv() here will require every UserService implementation to support reporting. Reporting looks like a separate capability and may deserve its own interface.*
- *The empty archive() implementation makes the operation appear successful even though nothing happens. If archival is unsupported, it should not be part of this implementation's contract.*
- *Returning null here conflates unsupported functionality with a missing result. Consider separating invoice generation into its own capability.*
- *This interface is becoming a collection of unrelated user operations. Could we separate profile, authentication, password management, and administration contracts?*
- *Please avoid adding a default method that throws UnsupportedOperationException. It keeps the broad contract intact rather than fixing the interface boundary.*
- *This service uses only findById(), so depending on the entire CustomerManagementService unnecessarily couples it to write operations.*
- *Can we model this provider as PaymentProcessor + RefundProcessor instead of requiring all payment providers to implement refund behavior?*
- *Before adding this method to the shared interface, please check whether all existing implementations can honor the same contract without dummy behavior.*
25. Code Review Exercise
Review the following Spring Boot integration code.
public interface CustomerDataProvider {
Customer getCustomer(Long customerId);
List<Customer> searchCustomers(String query);
Customer updateCustomer(Customer customer);
void deleteCustomer(Long customerId);
byte[] exportCustomers();
}
@Component
public class InternalCustomerDataProvider implements CustomerDataProvider {
private final CustomerRepository customerRepository;
public InternalCustomerDataProvider(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
@Override
public Customer getCustomer(Long customerId) {
return customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException(customerId));
}
@Override
public List<Customer> searchCustomers(String query) {
return customerRepository.search(query);
}
@Override
public Customer updateCustomer(Customer customer) {
return customerRepository.save(customer);
}
@Override
public void deleteCustomer(Long customerId) {
customerRepository.deleteById(customerId);
}
@Override
public byte[] exportCustomers() {
return generateCustomerExport();
}
private byte[] generateCustomerExport() {
return new byte[0];
}
}
@Component
public class PartnerCustomerDataProvider implements CustomerDataProvider {
private final PartnerCustomerClient partnerCustomerClient;
public PartnerCustomerDataProvider(PartnerCustomerClient partnerCustomerClient) {
this.partnerCustomerClient = partnerCustomerClient;
}
@Override
public Customer getCustomer(Long customerId) {
return partnerCustomerClient.getCustomer(customerId);
}
@Override
public List<Customer> searchCustomers(String query) {
return partnerCustomerClient.search(query);
}
@Override
public Customer updateCustomer(Customer customer) {
throw new UnsupportedOperationException("Partner data is read-only");
}
@Override
public void deleteCustomer(Long customerId) {
}
@Override
public byte[] exportCustomers() {
return null;
}
}
@Service
public class CustomerLookupService {
private final CustomerDataProvider customerDataProvider;
public CustomerLookupService(CustomerDataProvider customerDataProvider) {
this.customerDataProvider = customerDataProvider;
}
public Customer getCustomer(Long customerId) {
return customerDataProvider.getCustomer(customerId);
}
}Identify:
- Interface-design problems
- Code smells
- Unsupported behavior
- Runtime risks
- Maintainability issues
- Testability issues
- Possible security concerns
- Better interface boundaries
Do not reveal the solution until you have reviewed the code yourself.
26. Exercise Solution
The code contains several ISP-related problems.
Issue 1: CustomerDataProvider Is Too Broad
The interface combines:
- Reading
- Searching
- Updating
- Deleting
- Exporting
These are different capabilities.
Issue 2: Partner Provider Cannot Update Customers
The method:
updateCustomer()throws:
UnsupportedOperationExceptionThe implementation therefore cannot fully honor the interface.
Issue 3: Empty Delete Implementation
The partner provider contains:
public void deleteCustomer(Long customerId) {
}This is particularly dangerous.
A caller may believe the customer was deleted when nothing occurred.
Issue 4: exportCustomers Returns null
This hides unsupported export functionality.
Downstream code may fail with NullPointerException.
Issue 5: CustomerLookupService Depends on Too Much
It uses only:
getCustomer()but depends on an interface containing modification and deletion operations.
Issue 6: Security Boundary Is Broader Than Necessary
A lookup service has a dependency exposing deletion and update capabilities.
Authorization is still required at runtime, but the dependency itself exposes more privileged functionality than the consumer needs.
Improved Interfaces
Create separate capabilities.
public interface CustomerReader {
Customer getCustomer(Long customerId);
List<Customer> searchCustomers(String query);
}
public interface CustomerWriter {
Customer updateCustomer(Customer customer);
}
public interface CustomerDeleter {
void deleteCustomer(Long customerId);
}
public interface CustomerExporter {
byte[] exportCustomers();
}The internal provider can support all capabilities.
@Component
public class InternalCustomerService implements CustomerReader, CustomerWriter, CustomerDeleter, CustomerExporter {
private final CustomerRepository customerRepository;
private final CustomerExportService customerExportService;
public InternalCustomerService(CustomerRepository customerRepository, CustomerExportService customerExportService) {
this.customerRepository = customerRepository;
this.customerExportService = customerExportService;
}
@Override
public Customer getCustomer(Long customerId) {
return customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException(customerId));
}
@Override
public List<Customer> searchCustomers(String query) {
return customerRepository.search(query);
}
@Override
public Customer updateCustomer(Customer customer) {
return customerRepository.save(customer);
}
@Override
public void deleteCustomer(Long customerId) {
customerRepository.deleteById(customerId);
}
@Override
public byte[] exportCustomers() {
return customerExportService.exportCustomers();
}
}The partner provider implements only read functionality.
@Component
public class PartnerCustomerReader implements CustomerReader {
private final PartnerCustomerClient partnerCustomerClient;
public PartnerCustomerReader(PartnerCustomerClient partnerCustomerClient) {
this.partnerCustomerClient = partnerCustomerClient;
}
@Override
public Customer getCustomer(Long customerId) {
return partnerCustomerClient.getCustomer(customerId);
}
@Override
public List<Customer> searchCustomers(String query) {
return partnerCustomerClient.search(query);
}
}The lookup service depends only on read capability.
@Service
public class CustomerLookupService {
private final CustomerReader customerReader;
public CustomerLookupService(CustomerReader customerReader) {
this.customerReader = customerReader;
}
public Customer getCustomer(Long customerId) {
return customerReader.getCustomer(customerId);
}
}Why Each Change Is Useful
CustomerReader makes the read-only contract explicit.
The partner integration no longer contains:
- Unsupported updates
- Fake deletes
- Null exports
CustomerLookupService now communicates exactly what it needs.
Future export changes do not affect customer lookup.
Write access is no longer unnecessarily exposed through the lookup service's dependency.
Unit tests become simpler because the service can mock:
CustomerReaderrather than a large multi-purpose provider.
27. Interview Perspective
ISP commonly appears in Java interviews as a design or code-review scenario rather than a pure definition question.
An interviewer may ask:
We have an interface with ten methods, but some implementations throw UnsupportedOperationException for four methods. What would you change?
A strong answer should explain that interface size alone is not the primary issue.
The real issue is whether clients and implementations are being forced to depend on unsupported or unnecessary operations.
Another scenario may involve Spring Boot.
For example:
A controller only needs customer lookup but injects CustomerService, which contains create, update, delete, export, password reset, and role-management methods. Is that a problem?
A good answer should discuss:
- Narrow dependency contracts
- Reduced coupling
- Testability
- Least-privilege design
- Interface cohesion
- Dependency inversion
At senior level, interviewers may also ask when not to split an interface.
The correct response is that over-segregation creates unnecessary abstraction overhead.
Interfaces should be separated according to meaningful client responsibilities and capabilities, not arbitrary method-count rules.
28. Interview Questions and Answers
Basic Question
Question: What is the Interface Segregation Principle?
Answer:
ISP states that clients should not be forced to depend on methods they do not use.
In Java, this usually means designing focused interfaces instead of one large interface containing unrelated or unsupported operations.
Intermediate Question
Question: Is every large Java interface an ISP violation?
Answer:
No.
Interface size alone does not determine whether ISP is violated.
If all methods form one cohesive contract and clients genuinely need them together, a larger interface can be valid.
ISP becomes relevant when clients or implementations depend on methods they do not need or cannot support.
Advanced Question
Question: How are ISP and Liskov Substitution Principle related?
Answer:
Poor interface segregation often causes LSP problems.
If an interface contains operations not supported by every implementation, implementations may throw:
UnsupportedOperationExceptionor provide meaningless behavior.
Those implementations can no longer be safely substituted wherever the interface is expected.
Better interface segregation often restores substitutability by ensuring each implementation promises only capabilities it can genuinely provide.
Scenario-Based Question
Question: A payment interface contains pay(), refund(), capture(), and cancel(). Some payment providers support only pay() and refund(). How would you design it?
Answer:
I would model capabilities separately where the business differences are real.
For example:
PaymentProcessor
RefundProcessor
PaymentCaptureProcessor
PaymentCancellationProcessorA provider implements only the contracts it supports.
A service requiring refund functionality should depend on RefundProcessor, not a generic interface containing unsupported methods.
Code-Review Question
Question: What code smells suggest an ISP problem?
Answer:
I would investigate:
UnsupportedOperationException- Empty overridden methods
- Dummy return values
- Very large service interfaces
- Consumers using only one method from a large dependency
- Frequent interface changes affecting unrelated classes
- Default methods added only to avoid changing implementations
instanceofchecks used to detect supported capabilities- Read-only implementations forced to expose write methods
These are strong indicators that interface boundaries may be wrong.
Real-Project Question
Question: How would ISP improve a Spring Boot application?
Answer:
Instead of injecting broad services everywhere, Spring components can depend on focused contracts.
For example:
CustomerReader
CustomerWriter
CustomerExporterA reporting component depends only on CustomerReader.
An administration component may depend on CustomerWriter.
This reduces coupling, simplifies mocks, makes dependencies clearer, and prevents implementations from exposing unsupported capabilities.
Spring can still inject one implementation that implements several of these interfaces when appropriate.
29. Quick Rule to Remember
If a class implements methods it does not need, or a client receives methods it should never use, the interface is probably too broad.
30. Final Takeaway
The Interface Segregation Principle is not about creating the maximum possible number of interfaces.
It is about creating accurate contracts between clients and implementations.
Developers should remember:
- Interfaces should represent cohesive capabilities.
- Implementations should not be forced to provide unsupported behavior.
- Consumers should depend only on operations they actually require.
UnsupportedOperationException, empty overrides, and dummy values are strong interface-design warning signs.- One implementation can legitimately implement several focused interfaces.
- Large interfaces are acceptable when their methods genuinely belong together.
- Over-segregation should also be avoided.
During Pull Request review, reviewers should check:
- Whether every implementation can honor every interface method.
- Whether clients depend on methods they never use.
- Whether unsupported methods are hidden through exceptions or dummy behavior.
- Whether a broad service can be separated into meaningful capabilities.
- Whether read, write, administration, reporting, or provider-specific operations have been mixed unnecessarily.
- Whether the proposed interface will remain practical when new implementations are added.
In production Java systems, avoid abstractions where implementations are forced to say:
"I implement this interface, but half of these operations do not actually work."A good interface should describe a capability that every implementation can meaningfully and reliably provide.