Loading learning content...
Identifying split boundaries and knowing how to implement multiple interfaces are essential skills, but they address only half the challenge. In real-world systems, you rarely get to design interfaces from scratch. Instead, you inherit fat interfaces with dozens of clients, each depending on the current structure.
Migrating from fat interfaces to split interfaces is a delicate operation. Break the wrong thing, and you halt development for teams across your organization. Move too slowly, and the cost of the fat interface continues to compound.
This page teaches you battle-tested migration strategies that let you evolve interfaces incrementally, maintain system stability, and coordinate changes across teams—all without the dreaded "big bang" rewrite.
By the end of this page, you will master: (1) The Strangler Pattern for interface migration, (2) Incremental extraction with facade preservation, (3) Parallel run strategies for validation, (4) Deprecation workflows that guide adoption, and (5) Team coordination techniques for large-scale migrations.
Before diving into strategies, let's understand why interface migration is harder than it appears:
Technical Challenges:
Organizational Challenges:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// The fat interface we need to migrateinterface IUserService { // Authentication (used by: AuthController, SessionManager, LoginWidget) login(email: string, password: string): User; logout(userId: string): void; // Profile (used by: ProfilePage, SettingsController, AccountWidget) getProfile(userId: string): UserProfile; updateProfile(userId: string, profile: UserProfile): void; // Admin (used by: AdminPanel, UserModerationTool, ReportHandler) listAllUsers(): User[]; banUser(userId: string): void; deleteUser(userId: string): void;} // Current implementationclass UserService implements IUserService { /* ... 2000 lines */ } // CLIENT IMPACT ANALYSIS:// // Direct clients:// - AuthController (uses: login, logout) → src/controllers/auth.ts// - SessionManager (uses: login, logout) → src/services/session.ts// - ProfilePage (uses: getProfile, updateProfile) → src/pages/profile.tsx// - SettingsController (uses: updateProfile) → src/controllers/settings.ts// - AdminPanel (uses: listAllUsers, banUser, deleteUser) → src/admin/panel.tsx// - UserModerationTool (uses: banUser, deleteUser) → src/tools/moderation.ts// ... 15 more direct clients// // Transitive dependencies:// - @company/auth-sdk depends on IUserService types// - @company/admin-toolkit depends on IUserService// - Mobile app (React Native) shares interfaces via @company/shared-types// // Tests:// - 47 test files contain MockUserService// - Integration tests run against IUserService interface// // Total estimated impact: ~80 files, 3 teams, 2 external packages /** * GOAL: Split into focused interfaces: * - Authenticator * - ProfileManager * - UserAdministrator * * CONSTRAINT: Cannot break any of the above during migration */The temptation is to rewrite everything at once: split the interface, update all clients, merge one massive PR. This fails for non-trivial systems. The PR is too large to review, hidden dependencies break in production, and if anything goes wrong, rollback is impossible. Successful migrations are always INCREMENTAL.
The Strangler Pattern (named after the strangler fig tree that grows around and eventually replaces its host) lets you incrementally migrate from old interfaces to new ones without disrupting existing clients.
The Core Idea:
The old interface becomes a facade that routes calls to the new infrastructure. Clients can migrate at their own pace, and you can verify correctness at each step.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
// ============================================// PHASE 1: Create new, focused interfaces// (Old interface still exists untouched)// ============================================ // New focused interface 1: Authenticationinterface Authenticator { login(email: string, password: string): User; logout(userId: string): void;} // New focused interface 2: Profile Managementinterface ProfileManager { getProfile(userId: string): UserProfile; updateProfile(userId: string, profile: UserProfile): void;} // New focused interface 3: User Administrationinterface UserAdministrator { listAllUsers(): User[]; banUser(userId: string): void; deleteUser(userId: string): void;} // ============================================// PHASE 2: Create implementations for new interfaces// (Extract logic from UserService)// ============================================ class AuthenticationService implements Authenticator { constructor(private db: Database, private hasher: PasswordHasher) {} login(email: string, password: string): User { // Logic extracted from UserService.login() const user = this.db.users.findByEmail(email); if (!user || !this.hasher.verify(password, user.passwordHash)) { throw new AuthenticationError('Invalid credentials'); } return user; } logout(userId: string): void { // Logic extracted from UserService.logout() this.db.sessions.invalidateAll(userId); }} class ProfileService implements ProfileManager { constructor(private db: Database) {} getProfile(userId: string): UserProfile { // Logic extracted from UserService.getProfile() return this.db.profiles.findByUserId(userId); } updateProfile(userId: string, profile: UserProfile): void { // Logic extracted from UserService.updateProfile() this.db.profiles.update(userId, profile); }} class UserAdminService implements UserAdministrator { constructor(private db: Database, private auditLog: AuditLog) {} listAllUsers(): User[] { // Logic extracted from UserService.listAllUsers() return this.db.users.findAll(); } banUser(userId: string): void { // Logic extracted from UserService.banUser() this.db.users.setBanned(userId, true); this.auditLog.record({ action: 'ban', userId }); } deleteUser(userId: string): void { // Logic extracted from UserService.deleteUser() this.db.users.softDelete(userId); this.auditLog.record({ action: 'delete', userId }); }} // ============================================// PHASE 3: Transform old class into a facade// (Old interface delegates to new implementations)// ============================================ // BEFORE: IUserService was the real implementation// AFTER: IUserService becomes a thin facade /** * @deprecated Use Authenticator, ProfileManager, or UserAdministrator directly. * This class exists for backward compatibility during migration. * Will be removed in version 3.0. */class UserService implements IUserService { // New implementations composed internally private readonly auth: Authenticator; private readonly profile: ProfileManager; private readonly admin: UserAdministrator; constructor(db: Database, hasher: PasswordHasher, auditLog: AuditLog) { // Create internal instances of new services this.auth = new AuthenticationService(db, hasher); this.profile = new ProfileService(db); this.admin = new UserAdminService(db, auditLog); } // Delegate to new Authenticator login(email: string, password: string): User { return this.auth.login(email, password); } logout(userId: string): void { this.auth.logout(userId); } // Delegate to new ProfileManager getProfile(userId: string): UserProfile { return this.profile.getProfile(userId); } updateProfile(userId: string, profile: UserProfile): void { this.profile.updateProfile(userId, profile); } // Delegate to new UserAdministrator listAllUsers(): User[] { return this.admin.listAllUsers(); } banUser(userId: string): void { this.admin.banUser(userId); } deleteUser(userId: string): void { this.admin.deleteUser(userId); } // Expose new interfaces for migrating clients get authenticator(): Authenticator { return this.auth; } get profileManager(): ProfileManager { return this.profile; } get administrator(): UserAdministrator { return this.admin; }} // ============================================// PHASE 4: Migrate clients incrementally// ============================================ // BEFORE: Client uses IUserServiceclass OldAuthController { constructor(private userService: IUserService) {} handleLogin(email: string, password: string) { return this.userService.login(email, password); }} // AFTER: Client uses focused interfaceclass NewAuthController { constructor(private auth: Authenticator) {} handleLogin(email: string, password: string) { return this.auth.login(email, password); }} // During migration, both can coexist:// - OldAuthController still works (UserService facade routes to AuthenticationService)// - NewAuthController uses AuthenticationService directly// - Same underlying behavior, different dependency patterns // ============================================// PHASE 5: Remove facade once migration complete// ============================================ // After all clients have migrated:// 1. Remove UserService class// 2. Remove IUserService interface// 3. Update DI container configuration// 4. Clean up any remaining referencesThe facade (old UserService) ensures that unmigrated clients continue to work. You can deploy the new structure to production immediately—existing behavior is unchanged. Then migrate clients at whatever pace your organization can sustain. If problems arise, the facade means you haven't broken anything.
Sometimes you don't need to rewrite the implementation—you just need to expose different slices of the existing class through focused interfaces. This strategy is faster and lower-risk.
The Approach:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
// Original fat interfaceinterface IDocumentService { createDocument(title: string, content: string): Document; getDocument(id: string): Document; updateDocument(id: string, content: string): void; deleteDocument(id: string): void; searchDocuments(query: string): Document[]; exportToPDF(id: string): Buffer; exportToWord(id: string): Buffer; shareDocument(id: string, userId: string): void; getPermissions(id: string): Permissions;} // Original implementation (large, complex)class DocumentService implements IDocumentService { // ... 3000 lines of implementation} // ============================================// STEP 1: Define focused interfaces// (We're just defining contracts, not new implementations)// ============================================ interface DocumentRepository { createDocument(title: string, content: string): Document; getDocument(id: string): Document; updateDocument(id: string, content: string): void; deleteDocument(id: string): void;} interface DocumentSearcher { searchDocuments(query: string): Document[];} interface DocumentExporter { exportToPDF(id: string): Buffer; exportToWord(id: string): Buffer;} interface DocumentSharer { shareDocument(id: string, userId: string): void; getPermissions(id: string): Permissions;} // ============================================// STEP 2: Make existing class implement all interfaces// (No implementation changes needed!)// ============================================ class DocumentService implements IDocumentService, // Keep for backward compatibility DocumentRepository, DocumentSearcher, DocumentExporter, DocumentSharer{ // SAME 3000 lines of implementation // No changes required - it already implements all these methods!} // ============================================// STEP 3: Update DI configuration// (Same instance, multiple registrations)// ============================================ // Dependency Injection setupfunction configureDI(container: Container) { // Single instance of DocumentService const docService = new DocumentService(/* deps */); // Register as all interface types container.registerInstance<IDocumentService>(docService); container.registerInstance<DocumentRepository>(docService); container.registerInstance<DocumentSearcher>(docService); container.registerInstance<DocumentExporter>(docService); container.registerInstance<DocumentSharer>(docService); // All clients get the same instance, // but request only the interface they need} // ============================================// STEP 4: Migrate clients to focused interfaces// ============================================ // BEFORE: Editor takes the fat interfaceclass DocumentEditorOld { constructor(private docs: IDocumentService) {} open(id: string): void { const doc = this.docs.getDocument(id); // Uses 1 method // ... } save(id: string, content: string): void { this.docs.updateDocument(id, content); // Uses 1 method } // Total: uses 2 of 9 methods, depends on all 9} // AFTER: Editor takes focused interfaceclass DocumentEditorNew { constructor(private docs: DocumentRepository) {} open(id: string): void { const doc = this.docs.getDocument(id); // ... } save(id: string, content: string): void { this.docs.updateDocument(id, content); } // Total: uses 2 of 4 methods, depends on only 4} // BEFORE: Exporter takes fat interfaceclass PDFExporterOld { constructor(private docs: IDocumentService) {} export(id: string): Buffer { return this.docs.exportToPDF(id); // Uses 1 of 9 methods! }} // AFTER: Exporter takes focused interfaceclass PDFExporterNew { constructor(private exporter: DocumentExporter) {} export(id: string): Buffer { return this.exporter.exportToPDF(id); // Uses 1 of 2 methods }}Benefits of Extract Interface:
| Aspect | Strangler Pattern | Extract Interface |
|---|---|---|
| Implementation changes | Significant | None |
| Risk level | Moderate | Low |
| Speed | Slower (rewrite logic) | Faster (just add interfaces) |
| Resulting structure | Multiple classes | Single class, multiple interfaces |
| Best for | Complex refactoring | Simple interface cleanup |
Extract Interface is ideal when the existing implementation is sound but the INTERFACE is the problem—too broad, too coupled. If the implementation itself needs refactoring (separation of concerns, testability, etc.), the Strangler Pattern is more appropriate because it lets you rewrite as you migrate.
When migrating critical interfaces, you need confidence that the new implementation behaves identically to the old one. Parallel running executes both implementations simultaneously, comparing results to detect discrepancies before they become production incidents.
The Approach:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
// The interface being migratedinterface PaymentProcessor { processPayment(order: Order, method: PaymentMethod): PaymentResult; refund(transactionId: string, amount: Money): RefundResult; getTransaction(id: string): Transaction;} // Old implementation (battle-tested, complex, legacy)class LegacyPaymentProcessor implements PaymentProcessor { // 5 years of production usage, proven reliable // But: hard to test, tightly coupled, slow} // New implementation (clean, fast, well-tested)class ModernPaymentProcessor implements PaymentProcessor { // New architecture, better abstractions // But: not yet proven in production} // ============================================// PARALLEL RUN WRAPPER// Runs both, compares, returns legacy results// ============================================ class ParallelRunPaymentProcessor implements PaymentProcessor { constructor( private legacy: LegacyPaymentProcessor, private modern: ModernPaymentProcessor, private metrics: MetricsService, private logger: Logger ) {} processPayment(order: Order, method: PaymentMethod): PaymentResult { // STEP 1: Execute legacy (this is what we return) const legacyStart = Date.now(); const legacyResult = this.legacy.processPayment(order, method); const legacyDuration = Date.now() - legacyStart; // STEP 2: Execute modern in try-catch (errors don't affect response) setImmediate(async () => { try { const modernStart = Date.now(); const modernResult = await this.modern.processPayment(order, method); const modernDuration = Date.now() - modernStart; // STEP 3: Compare results const comparison = this.compare(legacyResult, modernResult); // STEP 4: Record metrics this.metrics.recordComparison({ operation: 'processPayment', orderId: order.id, match: comparison.matches, legacyDuration, modernDuration, differences: comparison.differences }); if (!comparison.matches) { this.logger.warn('Payment processing mismatch', { orderId: order.id, legacy: legacyResult, modern: modernResult, differences: comparison.differences }); } } catch (error) { this.logger.error('Modern payment processor failed', { orderId: order.id, error }); this.metrics.recordModernFailure('processPayment', error); } }); // STEP 5: Return legacy result (production traffic unchanged) return legacyResult; } refund(transactionId: string, amount: Money): RefundResult { // Same pattern: legacy result returned, modern compared async const legacyResult = this.legacy.refund(transactionId, amount); setImmediate(async () => { try { const modernResult = await this.modern.refund(transactionId, amount); this.compareAndRecord('refund', legacyResult, modernResult); } catch (error) { this.metrics.recordModernFailure('refund', error); } }); return legacyResult; } getTransaction(id: string): Transaction { // Read operations: safe to compare synchronously const legacyResult = this.legacy.getTransaction(id); try { const modernResult = this.modern.getTransaction(id); this.compareAndRecord('getTransaction', legacyResult, modernResult); } catch (error) { this.metrics.recordModernFailure('getTransaction', error); } return legacyResult; } private compare<T>(legacy: T, modern: T): ComparisonResult { // Deep comparison logic const differences: Difference[] = []; // Compare relevant fields based on type // Ignore timestamps, UUIDs that will naturally differ // Focus on business-relevant data return { matches: differences.length === 0, differences }; } private compareAndRecord<T>(op: string, legacy: T, modern: T): void { const comparison = this.compare(legacy, modern); this.metrics.recordComparison({ operation: op, match: comparison.matches }); if (!comparison.matches) { this.logger.warn(`${op} mismatch detected`, { legacy, modern }); } }} // ============================================// DASHBOARD METRICS// Track parallel run confidence over time// ============================================ interface ParallelRunDashboard { // Confidence metrics totalComparisons: number; matchingResults: number; matchPercentage: number; // Target: 99.99% // Performance metrics avgLegacyLatency: number; avgModernLatency: number; latencyImprovement: number; // Positive = modern is faster // Error metrics modernErrors: number; modernErrorRate: number; // Readiness assessment isReadyForCutover: boolean; // matchPercentage > 99.9% && modernErrorRate < 0.1%} // ============================================// GRADUAL CUTOVER// Once confidence is high, switch traffic// ============================================ class GradualCutoverPaymentProcessor implements PaymentProcessor { constructor( private legacy: LegacyPaymentProcessor, private modern: ModernPaymentProcessor, private featureFlags: FeatureFlags ) {} processPayment(order: Order, method: PaymentMethod): PaymentResult { // Feature flag controls which implementation is primary if (this.featureFlags.isEnabled('modern-payments', { orderId: order.id })) { // Gradual rollout: 1% → 5% → 25% → 50% → 100% return this.modern.processPayment(order, method); } return this.legacy.processPayment(order, method); }}Parallel running is essential for payment systems, financial calculations, security-critical paths, or any operation where discrepancies have significant consequences. The cost of running both implementations temporarily is far less than the cost of production bugs in critical flows.
Migration isn't just about technical mechanics—it's about organizational change management. Teams need motivation, guidance, and deadlines to update their code. A well-designed deprecation workflow provides all three.
The Deprecation Lifecycle:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
// ============================================// PHASE 1: ANNOUNCEMENT// Clear documentation of what's changing and why// ============================================ /** * @deprecated since version 2.4.0, will be removed in version 3.0.0 * * This monolithic interface is being split into focused interfaces: * - {@link Authenticator} for login/logout operations * - {@link ProfileManager} for profile operations * - {@link UserAdministrator} for admin operations * * Migration guide: https://docs.example.com/migrations/user-service-split * * Why this change? * 1. Reduces coupling - clients depend only on what they use * 2. Improves testability - mock only relevant interface * 3. Enables independent evolution of each concern * * Migration deadline: 2024-06-01 * * @see Authenticator * @see ProfileManager * @see UserAdministrator */interface IUserService { login(email: string, password: string): User; logout(userId: string): void; getProfile(userId: string): UserProfile; updateProfile(userId: string, profile: UserProfile): void; listAllUsers(): User[]; banUser(userId: string): void; deleteUser(userId: string): void;} // ============================================// PHASE 2: RUNTIME DEPRECATION WARNINGS// Help developers discover deprecated usage// ============================================ class DeprecatedUserService implements IUserService { private readonly newAuth: Authenticator; private readonly newProfile: ProfileManager; private readonly newAdmin: UserAdministrator; private readonly deprecationWarner: DeprecationWarner; constructor(deps: Dependencies) { this.newAuth = deps.authenticator; this.newProfile = deps.profileManager; this.newAdmin = deps.userAdministrator; this.deprecationWarner = new DeprecationWarner({ feature: 'IUserService', removalVersion: '3.0.0', migrationGuide: 'https://docs.example.com/migrations/user-service-split' }); } login(email: string, password: string): User { // Emit deprecation warning on first call this.deprecationWarner.warn( 'IUserService.login()', 'Use Authenticator.login() instead' ); return this.newAuth.login(email, password); } logout(userId: string): void { this.deprecationWarner.warn( 'IUserService.logout()', 'Use Authenticator.logout() instead' ); this.newAuth.logout(userId); } getProfile(userId: string): UserProfile { this.deprecationWarner.warn( 'IUserService.getProfile()', 'Use ProfileManager.getProfile() instead' ); return this.newProfile.getProfile(userId); } // ... similar for all methods} class DeprecationWarner { private warned = new Set<string>(); constructor(private config: DeprecationConfig) {} warn(method: string, replacement: string): void { if (this.warned.has(method)) return; // Warn once per method this.warned.add(method); const message = [ `[DEPRECATION WARNING] ${method} is deprecated.`, `Replacement: ${replacement}`, `Will be removed in: ${this.config.removalVersion}`, `Migration guide: ${this.config.migrationGuide}` ].join(''); console.warn(message); // Also report to centralized tracking analytics.trackDeprecationUsage({ feature: this.config.feature, method, callerStack: new Error().stack }); }} // ============================================// PHASE 3: MIGRATION TRACKING// Know who still needs to migrate// ============================================ interface MigrationTracker { // Track which clients still use deprecated interface getDeprecatedUsage(): UsageReport[]; // Track migration progress by team getMigrationProgress(): TeamProgress[]; // Alert teams approaching deadline sendMigrationReminders(): void;} interface UsageReport { interface: string; method: string; callSite: string; // File:line where deprecated method is called lastCalled: Date; callCount: number; team: string; // Owning team derived from code path} interface TeamProgress { team: string; totalUsages: number; migratedUsages: number; percentComplete: number; blockers: string[];} // Dashboard query exampleasync function getMigrationStatus(): Promise<MigrationDashboard> { const db = await getAnalyticsDB(); const usages = await db.query(` SELECT interface, method, call_site, team, COUNT(*) as calls FROM deprecation_usage WHERE timestamp > NOW() - INTERVAL '7 days' GROUP BY interface, method, call_site, team ORDER BY calls DESC `); return { totalLegacyUsages: usages.length, byTeam: groupByTeam(usages), blockedTeams: findBlockedTeams(usages), estimatedMigrationComplete: calculateCompletionDate(usages) };} // ============================================// PHASE 4: AUTOMATED MIGRATION ASSISTANCE// Make migrating as easy as possible// ============================================ // ESLint rule to detect deprecated usage// @company/eslint-rules/no-deprecated-user-service const noDeprecatedUserService = { meta: { type: 'problem', docs: { description: 'IUserService is deprecated. Use focused interfaces instead.', url: 'https://docs.example.com/migrations/user-service-split' }, fixable: 'code' // Provide auto-fix! }, create(context) { return { TSTypeReference(node) { if (node.typeName.name === 'IUserService') { context.report({ node, message: 'IUserService is deprecated. Use Authenticator, ProfileManager, or UserAdministrator.', suggest: [ // Context-aware suggestions based on usage ] }); } } }; }}; // Codemod for automated migration// npx jscodeshift -t ./codemods/migrate-user-service.ts src/ const migrateUserService: Transform = (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); // Find IUserService parameter types root.find(j.TSTypeReference, { typeName: { name: 'IUserService' } }) .forEach(path => { // Analyze usage to determine correct replacement const usedMethods = analyzeMethodUsage(path); if (usedMethods.every(m => ['login', 'logout'].includes(m))) { // Replace with Authenticator path.node.typeName.name = 'Authenticator'; } else if (usedMethods.every(m => ['getProfile', 'updateProfile'].includes(m))) { // Replace with ProfileManager path.node.typeName.name = 'ProfileManager'; } // ... more cases }); return root.toSource();};Effective deprecation combines positive incentives (the new interface is easier to use, better documented, faster) with clear consequences (warnings in tests, CI failures after deadline, calendar reminders). Make migration the path of least resistance.
In organizations with multiple teams, interface migrations require coordination across organizational boundaries. Here are proven strategies:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
// ============================================// MIGRATION TIMELINE EXAMPLE// ============================================ interface MigrationPlan { phases: MigrationPhase[];} const userServiceMigration: MigrationPlan = { phases: [ { name: "Phase 0: Preparation", duration: "2 weeks", tasks: [ "Create new focused interfaces", "Implement facade in UserService", "Write migration documentation", "Create codemod for common patterns", "Set up analytics tracking" ], successCriteria: "New interfaces available, docs published" }, { name: "Phase 1: Internal Migration", duration: "4 weeks", teams: ["Platform", "Identity"], // Teams that own the interface tasks: [ "Migrate all internal usages", "Update internal tests", "Validate with parallel running" ], successCriteria: "Zero deprecation warnings in CI for Platform/Identity" }, { name: "Phase 2: Pilot Team Migration", duration: "3 weeks", teams: ["Payments", "Notifications"], // Low-risk early adopters tasks: [ "Migrate pilot team services", "Gather feedback on migration experience", "Refine codemod based on edge cases" ], successCriteria: "Pilot teams fully migrated, positive feedback" }, { name: "Phase 3: General Availability", duration: "6 weeks", teams: ["All remaining teams"], tasks: [ "Announce migration window to all teams", "Hold weekly office hours", "Track progress by team", "Escalate blocked teams to leadership" ], successCriteria: "80% of usages migrated" }, { name: "Phase 4: Long Tail", duration: "4 weeks", tasks: [ "Personal outreach to remaining teams", "Centralized team fixes last stragglers", "Escalate to VP Engineering if blocked" ], successCriteria: "100% of usages migrated" }, { name: "Phase 5: Cleanup", duration: "2 weeks", tasks: [ "Remove deprecated IUserService interface", "Remove facade from UserService", "Remove analytics tracking", "Archive migration docs" ], successCriteria: "Legacy code completely removed" } ]}; // Total duration: ~21 weeks (5 months)// This is realistic for a major interface change// affecting 50+ files across 10+ teamsSend regular migration updates: what's changed, what's coming, who's blocked, who's done. Celebrate teams that complete early. Create friendly competition with leaderboards. Technical migration is often simpler than organizational migration—invest in communication.
Even with the best planning, migrations encounter blockers. Here are common blockers and strategies to address them:
| Blocker | Root Cause | Solution |
|---|---|---|
| "No bandwidth" | Team has other priorities | Get leadership to allocate time; offer centralized team help |
| Complex mocking setup | Tests tightly coupled to old interface | Provide mock generators; help refactor tests |
| Type inference dependencies | Code relies on types derived from old interface | Create type aliases that map old to new |
| External package dependency | Third-party code uses old interface | Fork/patch package; open upstream PR; create adapter |
| Generated code references | Code generators produce old interface usage | Update generators first; regenerate code |
| Fear of breaking changes | Team lacks confidence in testing | Offer to review changes; test in staging first |
| "Don't understand why" | Migration rationale not clear | 1:1 explanation; show concrete benefits for their code |
| Circular dependencies | New interfaces create dependency cycles | Restructure packages; use interface injection |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
// ============================================// BLOCKER: External library uses old interface// SOLUTION: Adapter pattern// ============================================ // Third-party library expects IUserServiceimport { ExternalAuthLibrary } from '@vendor/auth-lib'; // ExternalAuthLibrary.initialize(userService: IUserService) { ... } // Create an adapter that presents new interfaces as old interfaceclass UserServiceAdapter implements IUserService { constructor( private auth: Authenticator, private profile: ProfileManager, private admin: UserAdministrator ) {} // Implement IUserService by delegating to new interfaces login(email: string, password: string): User { return this.auth.login(email, password); } logout(userId: string): void { this.auth.logout(userId); } getProfile(userId: string): UserProfile { return this.profile.getProfile(userId); } // ... etc} // Usage: Vendor library gets adapter, rest of app uses new interfacesconst auth = new AuthenticationService(/* deps */);const profile = new ProfileService(/* deps */);const admin = new UserAdminService(/* deps */); // Vendor library uses adapterconst vendorCompatible = new UserServiceAdapter(auth, profile, admin);ExternalAuthLibrary.initialize(vendorCompatible); // Rest of application uses new interfaces directlyconst authController = new AuthController(auth);const profilePage = new ProfilePage(profile); // ============================================// BLOCKER: Type inference from old interface// SOLUTION: Type aliases for migration// ============================================ // Code that infers types from IUserServicetype LoginParams = Parameters<IUserService['login']>;type LoginReturn = ReturnType<IUserService['login']>; // Create equivalent aliases using new interfacetype LoginParamsNew = Parameters<Authenticator['login']>;type LoginReturnNew = ReturnType<Authenticator['login']>; // Migration shim: alias old types to new typestype MigrationLogin = typeof Authenticator.prototype.login;// Usage patterns can migrate gradually // ============================================// BLOCKER: Circular dependency created by split// SOLUTION: Interface injection / dependency inversion// ============================================ // PROBLEM: After split, AuthService needs ProfileService, // but ProfileService needs AuthService for permission checks // BAD: Direct dependency creates cycleclass AuthServiceBad { constructor(private profiles: ProfileService) {}} class ProfileServiceBad { constructor(private auth: AuthService) {} // Cycle!} // GOOD: Depend on interface, not implementationinterface PermissionChecker { canAccessProfile(userId: string, requesterId: string): boolean;} class ProfileServiceGood { constructor(private permissionChecker: PermissionChecker) {}} class AuthServiceGood implements PermissionChecker { canAccessProfile(userId: string, requesterId: string): boolean { // Implementation return true; }} // No cycle: ProfileService depends on interface, // AuthService implements interfaceWe've covered comprehensive strategies for migrating from fat interfaces to split interfaces in production systems. Here are the key takeaways:
Next Steps:
Migrations often require maintaining backward compatibility with clients that can't migrate immediately. The final page in this module covers backward compatibility considerations—how to evolve interfaces without breaking existing consumers, versioning strategies, and techniques for supporting multiple interface versions simultaneously.
You now have a comprehensive toolkit for migrating interfaces in production systems. The key insight: migrations are as much about organizational coordination as technical mechanics. Plan incrementally, communicate extensively, and always maintain a safe fallback path.