Repository Migration Strategy
Created: 2025-10-21 | Agent: Agent 2 (AAM) Tags: kmp repository-pattern migration-strategy architecture
Overview
The Repository Migration Strategy defines the conceptual approach for migrating repository layer code from Android-only (app/) to Kotlin Multiplatform shared modules (shared/domain, shared/data). This migration enables cross-platform code sharing while maintaining the existing offline-first, hybrid sync architecture.
Strategic Goals
1. Enable Cross-Platform Code Sharing
- Repository interfaces and implementations become platform-agnostic
- Business logic (validation, sync strategies) shared across Android, iOS, web
- Platform-specific code (Firebase SDKs, Context) isolated via abstractions
2. Maintain Offline-First Architecture
- Local Room database remains source of truth
- Hybrid repositories continue to combine local + remote seamlessly
- No user-facing changes (app still works fully offline)
3. Preserve Existing Patterns
- Manual DI (SharedModuleFactory) matches current RepositoryFactory
- Repository pattern unchanged (interface-based, testable)
- Hybrid sync strategy continues (no architectural shift)
4. Minimize Risk
- Incremental migration (one repository at a time)
- Comprehensive test coverage maintained (60% patch / 80% project)
- All existing tests must pass after each migration phase
Current State Analysis
Repository Inventory (14 Repositories)
1. TournamentRepository (Interface)
- Location:
app/src/main/java/.../domain/repository/TournamentRepository.kt - Type: Interface (52 methods across 8 concerns)
- Status: ⚠️ Agent 3 is splitting into 4 specialized repositories
- Migration: Wait for Agent 3’s split, then migrate clean architecture
2. RoundRepository (Class)
- Location:
app/src/main/java/.../domain/repository/RoundRepository.kt - Type: Class (needs interface extraction)
- Dependencies: RoundDao, BowSetupRepository
- Migration: Extract interface → Move to shared
3. Equipment Repositories (11 Classes)
- BowSetupRepository
- AccessoryRepository, ArrowRepository, BowStringRepository
- LimbsRepository, PlungerRepository, RestRepository
- RiserRepository, SightRepository, StabilizerRepository, WeightRepository
- Type: All classes (need interface extraction)
- Pattern: Take DAO dependencies, expose Flow/suspend methods
- Migration: Batch migration (similar patterns)
4. Support Classes
- RepositoryFactory (manual DI factory)
- RepositoryProvider (singleton provider)
- UpgradeRepairManager (database migration utility)
- Migration: Evolve into SharedModuleFactory
Dependency Management Strategy
Critical Dependencies
Agent 1 (Domain Models) - BLOCKING
Why:
- Repository interfaces reference domain models heavily
- Can't move interfaces without models in shared/domain first
Example:
interface RoundRepository {
suspend fun getRoundById(id: Int): Round? // Needs Round in shared/domain
fun getAllRounds(): Flow<List<Round>>
}
Status: ⏸️ Waiting for Agent 1 to complete
Estimated: 2-3 days
Agent 3 (Repository Split) - BLOCKING
Why:
- Agent 3 is splitting HybridTournamentRepository into 4 specialized repos NOW
- Migrating monolithic version would create merge conflicts
Agent 3's Split:
1. TournamentDiscoveryRepository (~280 lines)
2. TournamentCrudRepository (~320 lines)
3. TournamentParticipantRepository (~400 lines)
4. TournamentScoringRepository (~350 lines)
Status: ⏸️ Waiting for Agent 3's PR to merge
Estimated: 2-3 days
Migration Order (After Dependencies Resolve)
Phase 1: Interfaces First
- TournamentRepository (4 interfaces from Agent 3’s split)
- Extract RoundRepository interface
- Extract Equipment repository interfaces (batch)
Phase 2: Implementations Second
- Move tournament repository implementations
- Move RoundRepositoryImpl
- Move equipment repository implementations (batch)
Phase 3: Database Last
- Move DAOs (already interfaces)
- Configure Room KMP
- Test migrations
Rationale for Order:
- Interfaces are pure Kotlin (no platform dependencies)
- Implementations may need platform abstractions
- Database requires expect/actual setup (most complex)
Repository Split Integration (Agent 3’s Work)
Current: Monolithic HybridTournamentRepository
Problem:
- 1,809 lines, 52 methods
- 8+ distinct concerns (Discovery, CRUD, Participants, Scoring, Rounds, Settings, Moderation, Sync)
- Violates Single Responsibility Principle
- Hard to test, hard to maintain
Future: 4 Specialized Repositories + Coordinator
1. TournamentDiscoveryRepository
interface TournamentDiscoveryRepository {
fun getPublicTournaments(): Flow<List<Tournament>>
fun getMyTournaments(userId: String): Flow<List<Tournament>>
fun searchTournaments(query: String): Flow<List<Tournament>>
fun getTournamentsByStatus(status: TournamentStatus): Flow<List<Tournament>>
fun getTournamentsInDateRange(start: Long, end: Long): Flow<List<Tournament>>
suspend fun getTournament(id: TournamentId): Result<Tournament?>
fun observeTournament(id: TournamentId): Flow<Tournament?>
}Concern: Finding and browsing tournaments Lines: ~280 Pattern: Read-only hybrid (combines local + remote)
2. TournamentCrudRepository
interface TournamentCrudRepository {
suspend fun createTournament(tournament: Tournament): Result<TournamentId>
suspend fun updateTournament(tournament: Tournament): Result<Unit>
suspend fun deleteTournament(id: TournamentId): Result<Unit>
suspend fun cancelTournament(id: TournamentId, reason: String): Result<Unit>
suspend fun startTournament(id: TournamentId): Result<Unit>
suspend fun endTournament(id: TournamentId): Result<Unit>
suspend fun syncLocalTournament(localId: TournamentId): Result<Unit>
}Concern: Create, update, delete, lifecycle transitions Lines: ~320 Pattern: Offline-first with background sync
3. TournamentParticipantRepository
interface TournamentParticipantRepository {
suspend fun joinTournament(
tournamentId: TournamentId,
participant: TournamentParticipant,
authenticatedUserId: String? = null
): Result<Unit>
suspend fun retryTournamentJoinSync(
tournamentId: TournamentId,
participant: TournamentParticipant
): Result<Unit>
suspend fun joinTournamentWithCode(
joinCode: String,
participant: TournamentParticipant
): Result<Unit>
suspend fun leaveTournament(
tournamentId: TournamentId,
participantId: String
): Result<Unit>
fun getTournamentParticipants(tournamentId: TournamentId): Flow<List<TournamentParticipant>>
suspend fun getParticipant(tournamentId: TournamentId, participantId: String): Result<TournamentParticipant?>
suspend fun updateParticipantStatus(
tournamentId: TournamentId,
participantId: String,
status: ParticipantStatus
): Result<Unit>
}Concern: Participant management and join flow Lines: ~400 Pattern: Offline-first join with explicit auth state passing
4. TournamentScoringRepository
interface TournamentScoringRepository {
suspend fun submitScore(
tournamentId: TournamentId,
participantId: String,
roundNumber: Int,
arrowScores: List<Int>,
isXFlags: List<Boolean>,
endNumber: Int,
deviceId: String
): Result<Unit>
fun getTournamentLeaderboard(tournamentId: TournamentId): Flow<List<LeaderboardEntry>>
fun observeTournamentScores(
tournamentId: TournamentId,
roundNumber: Int
): Flow<List<TournamentScore>>
fun getParticipantRoundArrows(
tournamentId: TournamentId,
participantId: String,
roundNumber: Int
): Flow<List<EndScoreWithArrows>>
suspend fun invalidateTournamentCache(tournamentId: TournamentId)
}Concern: Score submission, leaderboards, real-time scoring Lines: ~350 Pattern: Hybrid with smart caching (TournamentScoreCacheService integration)
5. HybridTournamentRepository (Coordinator)
class HybridTournamentRepository(
private val discoveryRepo: TournamentDiscoveryRepository,
private val crudRepo: TournamentCrudRepository,
private val participantRepo: TournamentParticipantRepository,
private val scoringRepo: TournamentScoringRepository,
private val networkMonitor: NetworkMonitor
) {
// Façade methods that delegate to specialized repositories
fun getPublicTournaments() = discoveryRepo.getPublicTournaments()
suspend fun createTournament(t: Tournament) = crudRepo.createTournament(t)
suspend fun joinTournament(id, p) = participantRepo.joinTournament(id, p)
suspend fun submitScore(...) = scoringRepo.submitScore(...)
// Coordinator responsibilities:
// - Network monitoring
// - ID mapping (local ↔ Firebase)
// - Sync orchestration
// - Utility methods
}New Role: Façade coordinating 4 specialized repositories Lines: ~600-700 (61% reduction)
Migration Integration Strategy
When Agent 3 Completes:
- Pull latest main (get 4 specialized repository interfaces)
- Migrate 4 interfaces to
shared/domain/repository/tournament/ - Migrate 4 implementations to
shared/data/repository/tournament/ - Update HybridTournamentRepository to use shared repos
- Test all tournament flows (create, join, score, sync)
Benefits:
- Clean architecture from the start (no need to re-migrate)
- Single Responsibility Principle enforced
- Easier to test (smaller, focused repositories)
- Avoids merge conflicts
Interface Extraction Patterns
Pattern 1: Extract Interface from Class Repository
Current (Class-based):
// app/src/main/java/.../repository/RoundRepository.kt
class RoundRepository(
private val roundDao: RoundDao,
private val bowSetupRepository: BowSetupRepository
) {
suspend fun insertRound(round: Round): Long = roundDao.insertRound(round)
fun getAllRounds(): Flow<List<Round>> = roundDao.getAllRounds()
suspend fun getRoundById(roundId: Int): Round? = roundDao.getRoundById(roundId)
suspend fun updateRound(round: Round) = roundDao.updateRound(round)
suspend fun deleteRound(round: Round) {
bowSetupRepository.cleanupGuestSetupsForRound(round.id)
roundDao.deleteRound(round)
}
}Target (Interface + Implementation):
Interface:
// shared/domain/src/commonMain/kotlin/repository/RoundRepository.kt
interface RoundRepository {
suspend fun insertRound(round: Round): Long
fun getAllRounds(): Flow<List<Round>>
suspend fun getRoundById(roundId: Int): Round?
suspend fun updateRound(round: Round)
suspend fun deleteRound(round: Round)
}Implementation:
// shared/data/src/commonMain/kotlin/repository/RoundRepositoryImpl.kt
class RoundRepositoryImpl(
private val roundDao: RoundDao,
private val bowSetupRepository: BowSetupRepository
) : RoundRepository {
override suspend fun insertRound(round: Round): Long {
return roundDao.insertRound(round)
}
override fun getAllRounds(): Flow<List<Round>> {
return roundDao.getAllRounds()
}
override suspend fun getRoundById(roundId: Int): Round? {
return roundDao.getRoundById(roundId)
}
override suspend fun updateRound(round: Round) {
roundDao.updateRound(round)
}
override suspend fun deleteRound(round: Round) {
// Business logic: cleanup related data
bowSetupRepository.cleanupGuestSetupsForRound(round.id)
roundDao.deleteRound(round)
}
}Steps:
- Create interface in
shared/domainwith all public method signatures - Create implementation in
shared/dataextending the interface - Move implementation code from current class
- Update RepositoryFactory to provide interface (not class)
- Update all call sites to use interface type
Pattern 2: Equipment Repository Batch Migration
Observation: 11 equipment repositories follow identical pattern
Template:
// Interface (shared/domain)
interface [EquipmentType]Repository {
suspend fun insert(item: [EquipmentType]): Long
suspend fun update(item: [EquipmentType])
suspend fun delete(item: [EquipmentType])
suspend fun getById(id: Long): [EquipmentType]?
fun getAll(): Flow<List<[EquipmentType]>>
fun getAllActive(): Flow<List<[EquipmentType]>>
}
// Implementation (shared/data)
class [EquipmentType]RepositoryImpl(
private val dao: [EquipmentType]Dao
) : [EquipmentType]Repository {
override suspend fun insert(item: [EquipmentType]): Long = dao.insert(item)
override suspend fun update(item: [EquipmentType]) = dao.update(item)
override suspend fun delete(item: [EquipmentType]) = dao.delete(item)
override suspend fun getById(id: Long) = dao.getById(id)
override fun getAll() = dao.getAll()
override fun getAllActive() = dao.getAllActive()
}Batch Migration Strategy:
- Create all 11 interfaces in
shared/domain/repository/equipment/ - Create all 11 implementations in
shared/data/repository/equipment/ - Update SharedModuleFactory to provide all 11 repositories
- Test with existing equipment tests
- Update app RepositoryFactory to delegate to SharedModuleFactory
Time Savings: Batch approach saves ~2-3 days vs individual migration
Platform Abstraction Patterns
Identify Platform-Specific Code
Common Platform Dependencies:
- Android Context - Already removed by Agent 1 ✅
- Firebase SDKs - Need abstraction for iOS
- Logging - Need platform-agnostic logging
- Network connectivity - Already abstracted by Agent 1 ✅
Pattern: RemoteTournamentDataSource Abstraction
Problem: Firebase SDK is platform-specific (different for Android vs iOS)
Solution: Abstract remote data source interface
Interface (Shared):
// shared/data/src/commonMain/kotlin/datasources/RemoteTournamentDataSource.kt
interface RemoteTournamentDataSource {
suspend fun getTournament(id: TournamentId): Result<Tournament?>
fun observeTournament(id: TournamentId): Flow<Tournament?>
suspend fun createTournament(tournament: Tournament): Result<TournamentId>
suspend fun updateTournament(tournament: Tournament): Result<Unit>
suspend fun deleteTournament(id: TournamentId): Result<Unit>
fun getTournamentParticipants(id: TournamentId): Flow<List<TournamentParticipant>>
suspend fun submitScore(...): Result<Unit>
}Android Implementation:
// shared/data/src/androidMain/kotlin/datasources/FirebaseTournamentDataSource.kt
class FirebaseTournamentDataSource : RemoteTournamentDataSource {
private val firestore = Firebase.firestore
override suspend fun getTournament(id: TournamentId): Result<Tournament?> {
return try {
val doc = firestore.collection("tournaments")
.document(id)
.get()
.await()
Result.success(doc.toTournament())
} catch (e: Exception) {
Result.failure(e)
}
}
override fun observeTournament(id: TournamentId): Flow<Tournament?> {
return callbackFlow {
val listener = firestore.collection("tournaments")
.document(id)
.addSnapshotListener { snapshot, error ->
if (error != null) {
close(error)
} else {
trySend(snapshot?.toTournament())
}
}
awaitClose { listener.remove() }
}
}
// ... other methods
}iOS Implementation (Future):
// shared/data/src/iosMain/kotlin/datasources/FirebaseTournamentDataSource.kt
class FirebaseTournamentDataSource : RemoteTournamentDataSource {
// Use iOS Firebase SDK (Cocoapods dependency)
// Similar logic, different platform SDK
}Repository Uses Abstraction:
class TournamentCrudRepositoryImpl(
private val localDao: TournamentDao,
private val remoteDataSource: RemoteTournamentDataSource, // ✅ Interface
private val networkMonitor: NetworkMonitor
) : TournamentCrudRepository {
// Repository doesn't care which platform implementation is used
override suspend fun createTournament(tournament: Tournament): Result<TournamentId> {
val localId = localDao.insertTournament(tournament.toEntity())
if (networkMonitor.isOnline()) {
remoteDataSource.createTournament(tournament) // ✅ Platform-agnostic call
.onSuccess { firebaseId ->
idMappingDao.insert(localId, firebaseId)
}
}
return Result.success(localId)
}
}DI Integration:
// shared/di/src/androidMain/kotlin/AndroidSharedModuleFactory.kt
class AndroidSharedModuleFactory : SharedModuleFactory() {
override fun provideTournamentCrudRepository(): TournamentCrudRepository {
return TournamentCrudRepositoryImpl(
localDao = database.tournamentDao(),
remoteDataSource = FirebaseTournamentDataSource(), // ✅ Android implementation
networkMonitor = PlatformProvider.networkMonitor
)
}
}
// shared/di/src/iosMain/kotlin/IosSharedModuleFactory.kt (future)
class IosSharedModuleFactory : SharedModuleFactory() {
override fun provideTournamentCrudRepository(): TournamentCrudRepository {
return TournamentCrudRepositoryImpl(
localDao = database.tournamentDao(),
remoteDataSource = FirebaseTournamentDataSource(), // ✅ iOS implementation
networkMonitor = PlatformProvider.networkMonitor
)
}
}Risk Mitigation Patterns
Risk 1: Breaking Existing Tests
Mitigation Strategy:
- Run full test suite after each repository migration
- Update test imports incrementally
- Keep app tests passing (3,400+ tests)
- Add shared module tests in commonTest
Test Migration Pattern:
// Before (Android test)
// app/src/test/java/.../repository/RoundRepositoryTest.kt
class RoundRepositoryTest {
private val mockDao: RoundDao = mockk()
private val repository = RoundRepository(mockDao, mockk())
@Test
fun getRoundById_returnsRound() = runTest {
val expected = Round(id = 1, roundName = "Test")
coEvery { mockDao.getRoundById(1) } returns expected
val result = repository.getRoundById(1)
assertEquals(expected, result)
}
}
// After (Shared common test)
// shared/data/src/commonTest/kotlin/repository/RoundRepositoryImplTest.kt
class RoundRepositoryImplTest {
private val mockDao: RoundDao = mockk()
private val repository = RoundRepositoryImpl(mockDao, mockk())
@Test
fun getRoundById_returnsRound() = runTest {
val expected = Round(id = 1, roundName = "Test")
coEvery { mockDao.getRoundById(1) } returns expected
val result = repository.getRoundById(1)
assertEquals(expected, result)
}
}Keep Android tests for platform-specific code:
// app/src/test/java/.../repository/FirebaseTournamentDataSourceTest.kt
@RunWith(AndroidJUnit4::class)
class FirebaseTournamentDataSourceTest {
// Test Android Firebase SDK integration
}Risk 2: Merge Conflicts with Agent 3
Mitigation Strategy:
- WAIT for Agent 3 to complete repository split
- Pull main AFTER Agent 3’s PR merges
- Migrate clean, split architecture (not monolithic)
- No concurrent work on same files
Coordination Protocol:
- Agent 2 waits in “productive waiting” mode
- Agent 3 completes repository split
- Agent 3’s PR merges to main
- Agent 2 pulls main (gets split repos)
- Agent 2 begins repository migration (no conflicts)
Risk 3: Repository Interface Changes
Mitigation Strategy:
- Interface extraction must preserve ALL method signatures
- Use IDE refactoring tools (extract interface)
- Verify all call sites compile after extraction
- Run tests to catch behavioral changes
Verification Checklist:
- All method signatures preserved
- All return types match
- All parameter types match
- All suspend functions marked correctly
- All Flow return types correct
- All documentation preserved
Migration Phases (Detailed)
Phase 1: Repository Interfaces (2-3 days)
Prerequisites:
- ✅ Agent 1 completes domain model migration
- ✅ Agent 3 completes repository split
Tasks:
-
Migrate Tournament Interfaces (from Agent 3’s split)
- TournamentDiscoveryRepository.kt →
shared/domain/repository/tournament/ - TournamentCrudRepository.kt →
shared/domain/repository/tournament/ - TournamentParticipantRepository.kt →
shared/domain/repository/tournament/ - TournamentScoringRepository.kt →
shared/domain/repository/tournament/
- TournamentDiscoveryRepository.kt →
-
Extract RoundRepository Interface
- Create
shared/domain/repository/RoundRepository.kt - Extract all public method signatures
- Update imports to use shared/domain models
- Create
-
Extract Equipment Repository Interfaces (Batch)
- Create 11 interfaces in
shared/domain/repository/equipment/ - Use template pattern (all follow same structure)
- Create 11 interfaces in
Validation:
- All interfaces compile in shared/domain
- No Android-specific imports
- All model references point to shared/domain
Phase 2: Repository Implementations (2-3 days)
Tasks:
-
Migrate Tournament Implementations
- Move 4 implementations to
shared/data/repository/tournament/ - Abstract Firebase with RemoteTournamentDataSource
- Keep hybrid sync logic
- Move 4 implementations to
-
Create RoundRepositoryImpl
- Move implementation to
shared/data/repository/RoundRepositoryImpl.kt - Update package imports
- Remove any Android-specific code (logging)
- Move implementation to
-
Create Equipment Repository Implementations (Batch)
- Create 11 implementations in
shared/data/repository/equipment/ - Use template pattern
- Create 11 implementations in
Validation:
- All implementations compile in shared/data
- No Android SDK dependencies (except platform abstractions)
- All tests pass
Phase 3: Dependency Injection (1-2 days)
Tasks:
-
Extend SharedModuleFactory
- Add methods for all repository interfaces
- Provide singleton instances
-
Implement AndroidSharedModuleFactory
- Provide repository implementations
- Inject DAOs from database
- Inject platform abstractions
-
Update App RepositoryFactory
- Delegate to AndroidSharedModuleFactory
- Keep backward compatibility
- Update tests
Validation:
- All repositories accessible via SharedModuleFactory
- No circular dependencies
- Singletons work correctly
Phase 4: Testing & Validation (2-3 days)
Tasks:
-
Migrate Repository Tests to Common
- Move unit tests to
shared/data/src/commonTest/ - Update imports
- Run tests on JVM target
- Move unit tests to
-
Run Full Test Suite
- All app unit tests (3,400+ tests)
- All E2E tests (5 suites)
- All shared module tests
-
Integration Testing
- Test repository DI
- Test database access
- Test hybrid sync flows
Validation:
- All existing tests pass
- New shared tests pass
- Coverage maintained (60% patch / 80% project)
- E2E tests pass
Success Metrics
Code Migration
- All repository interfaces in
shared/domain(~1,000 lines) - All repository implementations in
shared/data(~2,200 lines) - SharedModuleFactory fully implemented
- App RepositoryFactory delegates to SharedModuleFactory
Build & Compilation
-
./gradlew :shared:domain:build- SUCCESS -
./gradlew :shared:data:build- SUCCESS -
./gradlew :app:build- SUCCESS - No Android SDK in shared/domain or shared/data
Testing
- All existing tests pass (3,400+ tests)
- New shared tests added
- Coverage maintained (60% patch / 80% project)
- E2E tests pass
Quality Gates
- Manual DI pattern maintained
- All detekt checks pass
- No merge conflicts with Agent 1 or Agent 3
- Clean architecture (SRP enforced)
Timeline Estimate
Total: 12-17 days (active work)
- Waiting for Dependencies: 3-5 days (Agent 1 & Agent 3)
- Phase 1 (Interfaces): 2-3 days
- Phase 2 (Implementations): 2-3 days
- Phase 3 (DI): 1-2 days
- Phase 4 (Testing): 2-3 days
- Phase 5 (Database): 3-4 days (covered in Room KMP Architecture)
- Phase 6 (Final Validation): 2-3 days
Critical Path: Agent 1 → Agent 3 → Agent 2
Related Documentation
Code Repository:
- Week 2 Migration Plan (Implementation details)
- Room KMP Migration Guide
- God Class Extraction Plans
Obsidian Vault:
- KMP Data Layer Architecture (Architecture overview)
- Room KMP Architecture (Database migration)
- KMP Migration Progress (Project status)
Last Updated: 2025-10-21 Status: Strategy complete, waiting for dependencies (Agent 1 & Agent 3) Next Steps: Pull main after Agent 1 & Agent 3 merge, begin Phase 1