Impact: MEDIUM (Monolithic interfaces increase coupling, bloat test mocks, and make refactoring risky)
The Interface Segregation Principle (ISP) states that no client should be forced to depend on methods it does not use. When a single repository interface defines every possible operation -- read, write, search, aggregate, archive -- every consumer and every test mock must account for that entire surface area. A service that only reads data still depends on an interface that includes delete and bulkUpdate. Splitting interfaces into focused contracts (ReadRepository, WriteRepository, SearchRepository) reduces coupling, simplifies testing, and makes it obvious what capabilities each consumer actually requires.
Incorrect (monolithic repository interface that forces all consumers to depend on everything):
// src/services/scan-summary-service.tsimport type { ScanRepository } from '@/domain/repositories/scan-repository';export class ScanSummaryService { // ❌ This service only calls findById and getAggregateStats, // but depends on the full 15-method interface constructor(private readonly scanRepo: ScanRepository) {} async getSummary(orgId: string): Promise<OrgScanSummary> { const stats = await this.scanRepo.getAggregateStats(orgId); return { organizationId: orgId, ...stats }; }}
// src/__tests__/scan-summary-service.test.ts// ❌ Test mock must implement all 15 methods even though the service uses 1const mockRepo: ScanRepository = { findById: jest.fn(), findByUserId: jest.fn(), findByOrganization: jest.fn(), search: jest.fn(), getAggregateStats: jest.fn().mockResolvedValue(mockStats), create: jest.fn(), update: jest.fn(), bulkUpdate: jest.fn(), delete: jest.fn(), bulkDelete: jest.fn(), archive: jest.fn(), restore: jest.fn(), getHistory: jest.fn(), export: jest.fn(), getRunningScans: jest.fn(),};
Correct (segregated interfaces composed where needed):
// src/services/scan-summary-service.tsimport type { ScanSearchRepository } from '@/domain/repositories/scan-search-repository';export class ScanSummaryService { // ✅ Depends only on the interface it actually uses constructor(private readonly scanSearch: ScanSearchRepository) {} async getSummary(orgId: string): Promise<OrgScanSummary> { const stats = await this.scanSearch.getAggregateStats(orgId); return { organizationId: orgId, ...stats }; }}
// src/__tests__/scan-summary-service.test.ts// ✅ Test mock only implements the 2 methods in ScanSearchRepositoryconst mockSearchRepo: ScanSearchRepository = { search: jest.fn(), getAggregateStats: jest.fn().mockResolvedValue(mockStats),};const service = new ScanSummaryService(mockSearchRepo);