Loading learning content...
We've explored the theory of role-based interface design: thinking in roles rather than implementations, designing for specific clients, and maintaining minimal interfaces. But how do we actually discover the right roles when designing a new system?
Role discovery is a systematic process that transforms domain understanding into well-designed interfaces. It bridges the gap between knowing what good interfaces look like and knowing how to create them. This is where ISP moves from principle to practice—a methodology you can apply to any system.
By the end of this page, you will have a step-by-step role discovery process, techniques for identifying roles from use cases and domain models, methods for validating discovered roles, and patterns for evolving interfaces as understanding deepens.
Role discovery follows a structured workflow that moves from domain understanding to validated interfaces. Here's the complete process:
┌─────────────────┐
│ 1. Identify │
│ Actors │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 2. Map Use │
│ Cases │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 3. Extract │
│ Capabilities │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 4. Group into │
│ Roles │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 5. Define │
│ Interfaces │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 6. Validate │
│ & Refine │
└─────────────────┘
Let's walk through each step with a detailed example: designing a Document Management System.
While presented linearly, role discovery is inherently iterative. You'll often loop back to earlier steps as understanding deepens. The first pass produces draft roles; refinement produces production-ready interfaces.
Actors are entities—people, systems, or automated processes—that interact with your system. Each actor type represents a distinct perspective with different needs.
For the Document Management System:
| Actor | Type | Primary Goal |
|---|---|---|
| Document Author | Human User | Create and edit documents |
| Document Reviewer | Human User | Review and comment on documents |
| Reader | Human User | Find and read published documents |
| Administrator | Human User | Manage users, permissions, system config |
| Search Engine Crawler | External System | Index document content for search |
| Backup System | Internal System | Archive documents for disaster recovery |
| Audit Log | Internal Process | Record document access and changes |
| Notification Service | Internal System | Alert users of document changes |
Techniques for Actor Identification:
Key insight: Each actor suggests distinct interface needs. The Author needs editing capabilities; the Reader only needs viewing. Mixing these creates fat interfaces.
Systems and automated processes are actors too. A backup system, a search indexer, or an analytics pipeline each represent distinct clients with specific needs. They often need very minimal interfaces compared to human users.
For each actor, enumerate the use cases—the specific things they need to accomplish. Use cases reveal what capabilities the system must provide from each actor's perspective.
Document Management System Use Cases:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
## Actor: Document Author- UC-A1: Create a new document- UC-A2: Edit document content- UC-A3: Upload attachments to document- UC-A4: Submit document for review- UC-A5: View feedback on document- UC-A6: Publish approved document ## Actor: Document Reviewer- UC-R1: View documents pending review- UC-R2: Read document content- UC-R3: Add comments to document- UC-R4: Approve or reject document- UC-R5: Request changes from author ## Actor: Reader- UC-RD1: Search for documents- UC-RD2: Browse document categories- UC-RD3: Read document content- UC-RD4: Download document attachments ## Actor: Administrator- UC-AD1: Manage user accounts- UC-AD2: Configure permissions- UC-AD3: View system analytics- UC-AD4: Archive old documents- UC-AD5: Restore archived documents ## Actor: Search Engine Crawler- UC-S1: Get list of all published documents- UC-S2: Get document metadata- UC-S3: Get document content for indexing ## Actor: Backup System- UC-B1: List all documents (including drafts)- UC-B2: Get full document data with versioning- UC-B3: Restore document from backup ## Actor: Audit Log- UC-AU1: Record document creation- UC-AU2: Record document modification- UC-AU3: Record document access- UC-AU4: Record permission changes ## Actor: Notification Service- UC-N1: Get documents with recent changes- UC-N2: Get subscribers for a document- UC-N3: Record notification sentNotice the patterns:
These clusters foreshadow the roles we'll discover.
Use cases should be specific enough to reveal capabilities but not so granular that they become implementation details. 'Create a new document' is good; 'Click the new button' is too implementation-specific; 'Use the document system' is too vague.
Transform use cases into capabilities—the specific operations the system must support. Capabilities are verb-noun pairs that describe actions on resources.
Capability Extraction Process:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// Use Case → Capability Mapping // Author Use Cases → Capabilities// UC-A1: Create a new document → createDocument(metadata): DocumentId// UC-A2: Edit document content → updateContent(docId, content): void// UC-A3: Upload attachments → addAttachment(docId, file): AttachmentId// UC-A4: Submit for review → submitForReview(docId): void// UC-A5: View feedback → getComments(docId): Comment[]// UC-A6: Publish document → publishDocument(docId): void // Reviewer Use Cases → Capabilities// UC-R1: View pending reviews → getPendingReviews(reviewerId): Document[]// UC-R2: Read document content → getContent(docId): Content// UC-R3: Add comments → addComment(docId, comment): CommentId// UC-R4: Approve/reject → setReviewDecision(docId, decision): void// UC-R5: Request changes → requestChanges(docId, feedback): void // Reader Use Cases → Capabilities// UC-RD1: Search documents → searchDocuments(query): SearchResult[]// UC-RD2: Browse categories → listByCategory(category): Document[]// UC-RD3: Read content → getPublishedContent(docId): Content// UC-RD4: Download attachments → getAttachment(attachmentId): File // Administrator Use Cases → Capabilities// UC-AD1: Manage users → createUser/updateUser/deleteUser// UC-AD2: Configure permissions → setPermissions(userId, perms): void// UC-AD3: View analytics → getSystemMetrics(): Metrics// UC-AD4: Archive documents → archiveDocument(docId): void// UC-AD5: Restore documents → restoreDocument(docId): void // Search Engine Crawler → Capabilities// UC-S1: List published → listPublishedDocuments(): DocumentRef[]// UC-S2: Get metadata → getDocumentMetadata(docId): Metadata// UC-S3: Get content for indexing → getIndexableContent(docId): string // Backup System → Capabilities // UC-B1: List all documents → listAllDocuments(): DocumentRef[]// UC-B2: Get full data → exportDocument(docId): ExportData// UC-B3: Restore from backup → importDocument(data): DocumentId // Audit Log → Capabilities// UC-AU1-4: Record events → recordEvent(event: AuditEvent): void // Notification Service → Capabilities// UC-N1: Get changes → getRecentChanges(since: Date): Change[]// UC-N2: Get subscribers → getDocumentSubscribers(docId): User[]// UC-N3: Record sent → markNotified(docId, userId): voidCapability Categories:
| Category | Description | Examples |
|---|---|---|
| Query | Retrieves information without side effects | getContent, searchDocuments, listByCategory |
| Command | Modifies system state | createDocument, updateContent, archiveDocument |
| Event | Records something that happened | recordEvent, markNotified |
These categories often suggest interface boundaries (CQRS-style split).
Multiple use cases often map to the same capability (UC-R2 and UC-RD3 both need 'read content'). This is fine—the grouping step will consolidate them. Don't force distinct capabilities for every use case.
Now we cluster capabilities into roles—coherent sets of related operations that go together. Roles emerge from analyzing:
Role Discovery for Document Management:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
// ROLE GROUPING ANALYSIS // Group 1: Document Reading (Reader, Reviewer, Author all need these)// - getContent// - getPublishedContent // - getAttachment// → Role: DocumentReader // Group 2: Document Writing (Only Author needs these)// - createDocument// - updateContent// - addAttachment// → Role: DocumentWriter // Group 3: Document Workflow (Author submits, Reviewer decides)// - submitForReview// - getPendingReviews// - getComments / addComment// - setReviewDecision / requestChanges// - publishDocument// → Role: WorkflowManager (or split further) // Group 4: Document Search (Reader, Crawler need these)// - searchDocuments// - listByCategory// - listPublishedDocuments// → Role: DocumentSearcher // Group 5: User Administration (Only Admin)// - createUser / updateUser / deleteUser// - setPermissions// → Role: UserAdministrator // Group 6: Document Administration (Only Admin)// - archiveDocument / restoreDocument// - getSystemMetrics// → Role: DocumentAdministrator // Group 7: Indexing Support (Only Crawler)// - listPublishedDocuments// - getDocumentMetadata// - getIndexableContent// → Role: IndexableContent // Group 8: Backup Operations (Only Backup System)// - listAllDocuments// - exportDocument// - importDocument// → Role: BackupProvider // Group 9: Audit Recording (Only Audit Log)// - recordEvent// → Role: AuditRecorder // Group 10: Change Tracking (Notification Service)// - getRecentChanges// - getDocumentSubscribers// - markNotified// → Role: ChangeTrackerRole Grouping Heuristics:
Actor Alignment: If only one actor type uses a set of capabilities, they form a strong candidate role
Semantic Cohesion: Capabilities that operate on the same conceptual level belong together (e.g., all review-related operations)
Access Pattern: Read-only capabilities can often be a separate role from write capabilities
Change Frequency: Capabilities that change for the same business reason should be in the same role
Security Boundary: Capabilities with the same permission level often form a role
The same capability can appear in multiple roles if different actors need it. For example, 'getContent' might be in both DocumentReader (for viewing) and IndexableContent (for search indexing). The shared capability doesn't mean the roles should merge—the contexts are different.
Transform discovered roles into formal interfaces. Each role becomes an interface with method signatures derived from capabilities.
Interface Design Guidelines:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
// ROLE → INTERFACE TRANSFORMATION // Role: DocumentReader → Interfaceinterface DocumentReader { getContent(documentId: DocumentId): DocumentContent; getAttachment(attachmentId: AttachmentId): Attachment; exists(documentId: DocumentId): boolean;} // Role: DocumentWriter → Interfaceinterface DocumentWriter { createDocument(metadata: DocumentMetadata): DocumentId; updateContent(documentId: DocumentId, content: DocumentContent): void; addAttachment(documentId: DocumentId, file: FileUpload): AttachmentId; deleteDocument(documentId: DocumentId): void;} // Role: DocumentSearcher → Interfaceinterface DocumentSearcher { search(query: SearchQuery): SearchResult[]; listByCategory(category: Category): DocumentSummary[]; listAll(options?: ListOptions): DocumentSummary[];} // Role: WorkflowManager → Interface// This might be split further based on reviewinterface ReviewWorkflow { submitForReview(documentId: DocumentId): void; getPendingReviews(reviewerId: UserId): DocumentSummary[]; addReviewComment(documentId: DocumentId, comment: ReviewComment): CommentId; setDecision(documentId: DocumentId, decision: ReviewDecision): void;} interface PublishingWorkflow { publish(documentId: DocumentId): void; unpublish(documentId: DocumentId): void; isPublished(documentId: DocumentId): boolean;} // Role: UserAdministrator → Interfaceinterface UserAdministrator { createUser(userData: UserData): UserId; updateUser(userId: UserId, updates: Partial<UserData>): void; deleteUser(userId: UserId): void; setPermissions(userId: UserId, permissions: PermissionSet): void;} // Role: DocumentAdministrator → Interfaceinterface DocumentAdministrator { archive(documentId: DocumentId): void; restore(documentId: DocumentId): void; getMetrics(): SystemMetrics;} // Role: IndexableContent → Interface (for Search Crawler)interface IndexableContentProvider { listIndexableDocuments(): DocumentReference[]; getMetadata(documentId: DocumentId): IndexableMetadata; getTextContent(documentId: DocumentId): string;} // Role: BackupProvider → Interfaceinterface DocumentBackupProvider { listAllForBackup(): DocumentReference[]; export(documentId: DocumentId): ExportedDocument; import(data: ExportedDocument): DocumentId;} // Role: AuditRecorder → Interfaceinterface AuditRecorder { recordEvent(event: AuditEvent): void;} // Role: ChangeTracker → Interfaceinterface ChangeTracker { getChangesSince(timestamp: Date): DocumentChange[]; getSubscribers(documentId: DocumentId): Subscriber[]; markNotified(documentId: DocumentId, userId: UserId): void;}Result Analysis:
| Interface | Method Count | Primary Client |
|---|---|---|
| DocumentReader | 3 | Everyone who reads |
| DocumentWriter | 4 | Document Authors |
| DocumentSearcher | 3 | Readers, UI |
| ReviewWorkflow | 4 | Authors, Reviewers |
| PublishingWorkflow | 3 | Authors |
| UserAdministrator | 4 | Admins |
| DocumentAdministrator | 3 | Admins |
| IndexableContentProvider | 3 | Search Crawler |
| DocumentBackupProvider | 3 | Backup System |
| AuditRecorder | 1 | Audit Service |
| ChangeTracker | 3 | Notification Service |
All interfaces have 1-4 methods. Each serves a specific client category. No interface forces clients to depend on methods they don't use.
By deriving interfaces from client-specific roles rather than implementation capabilities, we naturally achieve ISP compliance. The principle emerges from the process, not from retrofitting.
Before finalizing interfaces, validate them against real usage scenarios. This step catches design issues before they become embedded in code.
Validation Techniques:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
// VALIDATION: Use Case UC-A4 (Author submits document for review) // Original Use Case:// "Author creates a document, adds content, then submits for review" // Walkthrough with discovered interfaces: class DocumentAuthoringService { constructor( private writer: DocumentWriter, private reader: DocumentReader, private workflow: ReviewWorkflow ) {} async createAndSubmitDocument( metadata: DocumentMetadata, content: DocumentContent ): Promise<DocumentId> { // Step 1: Create document const docId = this.writer.createDocument(metadata); // Step 2: Add content this.writer.updateContent(docId, content); // Step 3: Verify it's saved (optional) if (!this.reader.exists(docId)) { throw new Error('Document creation failed'); } // Step 4: Submit for review this.workflow.submitForReview(docId); return docId; }} // ✅ VALIDATED: Use case is fully implementable// ✅ Dependencies are minimal (3 role interfaces)// ✅ Each interface contributes to the use case// ✅ No unused methods forced on this client // VALIDATION: Use Case UC-RD1 (Reader searches for documents) class DocumentSearchService { constructor( private searcher: DocumentSearcher, private reader: DocumentReader ) {} async searchAndPreview(query: string): Promise<SearchPreview[]> { // Step 1: Search const results = this.searcher.search({ text: query }); // Step 2: Get preview content for top results return results.slice(0, 10).map(result => ({ ...result, preview: this.getPreviewText(result.documentId) })); } private getPreviewText(docId: DocumentId): string { const content = this.reader.getContent(docId); return content.text.substring(0, 200); }} // ✅ VALIDATED: Only needs Searcher and Reader// ✅ Doesn't depend on Writer, Workflow, or Admin interfaces// ✅ Minimal, focused dependenciesCommon Refinement Patterns:
| Finding | Refinement |
|---|---|
| Too many interfaces for one use case | Consider merging related roles |
| Client needs method from two unrelated interfaces | Missing a composed interface or role |
| One interface has 7+ methods | Split by read/write or by sub-domain |
| Interface name is vague | Clarify the role; rename or split |
| Mock implementation is complex | Interface might conflate multiple concerns |
Writing unit tests against your interfaces is the ultimate validation. If tests are awkward to write—requiring many mocks or complex setup—the interfaces need refinement. Clean tests indicate clean interfaces.
Let's walk through a complete role discovery example for a different domain: an E-Commerce Shopping Cart System.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
// STEP 1: IDENTIFY ACTORS // Actors:// 1. Customer - adds items, manages cart, checks out// 2. Inventory System - checks/reserves stock// 3. Pricing Engine - calculates prices, applies discounts// 4. Analytics Service - tracks cart behavior// 5. Recommendation Engine - suggests related products // STEP 2: MAP USE CASES // Customer:// - UC-C1: Add item to cart// - UC-C2: Remove item from cart // - UC-C3: Update item quantity// - UC-C4: View cart contents// - UC-C5: Apply coupon code// - UC-C6: Calculate total// - UC-C7: Proceed to checkout // Inventory System:// - UC-I1: Check stock availability for cart items// - UC-I2: Reserve stock during checkout // Pricing Engine:// - UC-P1: Get current prices for items// - UC-P2: Apply bulk discounts// - UC-P3: Validate coupon codes // Analytics:// - UC-A1: Track item added to cart// - UC-A2: Track cart abandonment // Recommendations:// - UC-R1: Get cart items to suggest related products // STEP 3: EXTRACT CAPABILITIES // From Customer: addItem, removeItem, updateQuantity, getItems, // applyCoupon, getTotal, createOrder// From Inventory: checkStock, reserveStock// From Pricing: getPrice, calculateDiscounts, validateCoupon// From Analytics: recordEvent// From Recommendations: getCartItems // STEP 4: GROUP INTO ROLES // Role: CartModifier (Customer write operations)// - addItem, removeItem, updateQuantity, clear // Role: CartReader (Customer + Recommendations read operations)// - getItems, getItemCount, isEmpty // Role: CartPricer (Combined pricing)// - getTotal, getSubtotal, getTaxes, getDiscounts // Role: CouponApplicator (Coupon operations)// - applyCoupon, removeCoupon, getAppliedCoupons // Role: StockChecker (Inventory read)// - checkAvailability, getStockLevels // Role: StockReserver (Inventory write)// - reserve, releaseReservation // Role: CartEventRecorder (Analytics)// - recordEvent // STEP 5: DEFINE INTERFACES interface CartModifier { addItem(productId: ProductId, quantity: number): void; removeItem(itemId: CartItemId): void; updateQuantity(itemId: CartItemId, quantity: number): void; clear(): void;} interface CartReader { getItems(): ReadonlyArray<CartItem>; getItemCount(): number; isEmpty(): boolean; findItem(productId: ProductId): CartItem | undefined;} interface CartPricer { getSubtotal(): Money; getDiscounts(): Money; getTaxes(): Money; getTotal(): Money; getLineItemPrice(itemId: CartItemId): Money;} interface CouponApplicator { applyCoupon(code: CouponCode): CouponResult; removeCoupon(code: CouponCode): void; getAppliedCoupons(): ReadonlyArray<AppliedCoupon>;} interface StockChecker { checkAvailability(productId: ProductId, quantity: number): Availability; getStockLevel(productId: ProductId): number;} interface StockReserver { reserve(items: ReservationRequest[]): Reservation; release(reservationId: ReservationId): void;} interface CartEventRecorder { recordEvent(event: CartEvent): void;} // STEP 6: VALIDATE // Customer add-to-cart flow:class AddToCartUseCase { constructor( private modifier: CartModifier, private reader: CartReader, private stockChecker: StockChecker, private events: CartEventRecorder ) {} execute(productId: ProductId, quantity: number): AddToCartResult { // Check stock const availability = this.stockChecker.checkAvailability(productId, quantity); if (!availability.available) { return { success: false, reason: 'out_of_stock' }; } // Add to cart this.modifier.addItem(productId, quantity); // Record event this.events.recordEvent({ type: 'item_added', productId, quantity, cartItemCount: this.reader.getItemCount() }); return { success: true }; }} // ✅ Dependencies are focused: 4 role interfaces, each with clear purpose// ✅ No unused methods in any interface for this use case// ✅ Easy to mock for testingBy following the systematic role discovery process, we naturally arrive at ISP-compliant interfaces. The discipline of thinking through actors, use cases, and capabilities before writing code leads to designs that are clean from the start.
Role discovery isn't a one-time activity. As systems evolve, new actors emerge, use cases change, and interfaces must adapt. Here's how to evolve interfaces while maintaining ISP compliance:
1. Adding New Capabilities
When adding capabilities, ask: Does this belong to an existing role or define a new one?
1234567891011121314151617181920212223242526
// New requirement: Support gift wrapping // Option A: Add to existing role (if it fits semantically)interface CartModifier { addItem(productId: ProductId, quantity: number): void; removeItem(itemId: CartItemId): void; updateQuantity(itemId: CartItemId, quantity: number): void; clear(): void; setGiftWrap(itemId: CartItemId, wrap: GiftWrapOptions): void; // NEW}// ✅ Good if gift wrap is a core cart modification // Option B: Create new role interface (if it's a distinct concern)interface GiftOptionsManager { setGiftWrap(itemId: CartItemId, options: GiftWrapOptions): void; removeGiftWrap(itemId: CartItemId): void; addGiftMessage(itemId: CartItemId, message: string): void; getGiftOptions(itemId: CartItemId): GiftOptions;}// ✅ Good if gift options may grow into their own feature set // Option C: Extend with a new sub-interface (backward compatible)interface EnhancedCartModifier extends CartModifier { setGiftWrap(itemId: CartItemId, wrap: GiftWrapOptions): void;}// ✅ Good for gradual migration without breaking existing clients2. Splitting Growing Interfaces
If an interface grows beyond 5-6 methods, consider splitting:
1234567891011121314151617181920212223242526272829303132333435363738394041
// Before: CartModifier has grown too largeinterface CartModifier { addItem(productId: ProductId, quantity: number): void; removeItem(itemId: CartItemId): void; updateQuantity(itemId: CartItemId, quantity: number): void; clear(): void; setGiftWrap(itemId: CartItemId, options: GiftWrapOptions): void; addGiftMessage(itemId: CartItemId, message: string): void; setShippingPreference(itemId: CartItemId, pref: ShippingPreference): void; addNote(itemId: CartItemId, note: string): void; markForLater(itemId: CartItemId): void; moveToCart(itemId: CartItemId): void; // 10+ methods - too many!} // After: Split by concerninterface CartItemManager { addItem(productId: ProductId, quantity: number): void; removeItem(itemId: CartItemId): void; updateQuantity(itemId: CartItemId, quantity: number): void; clear(): void;} interface CartGiftManager { setGiftWrap(itemId: CartItemId, options: GiftWrapOptions): void; removeGiftWrap(itemId: CartItemId): void; addGiftMessage(itemId: CartItemId, message: string): void;} interface CartPreferencesManager { setShippingPreference(itemId: CartItemId, pref: ShippingPreference): void; addNote(itemId: CartItemId, note: string): void;} interface SaveForLaterManager { saveForLater(itemId: CartItemId): void; moveToCart(itemId: CartItemId): void; getSavedItems(): CartItem[];} // Clients now depend only on the specific managers they needDon't aim for perfect interfaces on the first try. Plan for evolution. Use smaller interfaces that can be composed, extended, or split as understanding grows. The role discovery process can be re-run periodically as the system matures.
Role discovery transforms ISP from an abstract principle into a concrete methodology. By following a structured process, you systematically derive interfaces that are ISP-compliant by construction.
Module Complete:
You've now learned the complete picture of role-based interface design:
With these tools, you can design systems where every interface serves a clear purpose, every client depends only on what it uses, and the architecture remains flexible as requirements evolve.
You now have a complete toolkit for role-based interface design. From conceptual understanding (roles, not implementations) to practical application (discovery process), you can design ISP-compliant interfaces for any system. The next module will explore techniques for splitting existing fat interfaces—bringing ISP to legacy code.